- 投稿日:2020-08-14T22:16:08+09:00
【Rails】ハッシュや配列に特定要素が存在するか判別 [ActiveRecord] [has_attribute?]
ハッシュや配列の中に特定の要素が存在するか確認する方法です。
emailが登録されていなかったら登録、されていたらメッセージ、みたいなイメージです。任意のハッシュを判別
このようなハッシュを例にします。
nilemptyに注目です。@user = { "id" => 1, "name" => "Taro", "age" => 26, "from" => nil, "sex" => "", "created_at" => Sun, 21 Aug 2020 09:55:05, "updated_at" => Sun, 21 Aug 2020 09:55:05 }has_attribute?で判別
特定の要素自体があるかどうか判別します。
has_attribute?でこのような結果が得られます。@user.has_attribute?(:name) # => true @user.has_attribute?(:from) # => true @user.has_attribute?(:sex) # => true @user.has_attribute?(:userid) # => false少しややこしいのが、中身が
nilemptyだとしても結果はtrueというところです。
要素自体があるかないかの判別ということです。attribute_present?で判別
特定の要素の中身が存在するか判別します。
attribute_present?でこのような結果が得られます。@user.attribute_present?(:name) # => true @user.attribute_present?(:from) # => false @user.attribute_present?(:sex) # => false @user.attribute_present?(:userid) # => エラー要素は存在する前提で、中身が空かどうか判別します。
参考
ActiveRecordやハッシュ・配列などで使えるメソッドは他にもたくさんありますよー
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods.html
- 投稿日:2020-08-14T19:36:05+09:00
assign_attributes で中間テーブルが保存されてしまう
- 入力フォームから値を受け取り、バリデーションを行ってから保存したい、みたいな時に一旦インスタンスの変数を更新するために
assign_attributesを利用したら思わぬ結果になった- rails の中間テーブルにおいては、
assign_attributes実行時にDBインサートが走り、コミットまでされてしまう
- そのため、modelのバリデーションエラーで保存に失敗した際に、中間テーブルが保存されっぱなしの状態になってしまった
- 以下のように理解していたが、これは正確ではなさそう
update_attributesはインスタンス変数の更新を行い、DBの更新まで行うassign_attributesはインスタンス変数の更新を行うが、DBの更新は行わない- つまり、
assign_attributesしてからバリデーションにかかったり、何らかの理由で保存されたくない時は明示的にロールバックするか、先にインサートしないという工夫が必要になるユースケース
- 以下のようなユースケースを想定
UserとGroupが多対多で、中間テーブルのモデルとしてGroupUsersが存在Userを保存する際に、同時にGroupUsersも保存したい- view から受け取ったパラメータをセットし保存を行うが、バリデーションエラーの時は保存せずエラーを返す
groups.rbclass Group < ApplicationRecord has_many :group_users has_many :users, through: :group_users endusers.rbclass User < ApplicationRecord has_many :group_users has_many :groups, through: :group_users validates :name, presence: true endgroup_users.rbclass GroupUsers < ApplicationRecord belongs_to :group belongs_to :user end
- User に以下のフィールドが生えるが、そのフィールドに値をセットするとインサートが走ってしまう
- groups
- group_ids
- group_users
- group_user_ids
案1 transaction を張る
assign_attributesからsaveまでを transaction で囲んで、保存できない時はロールバックするようにするusers_controller.rbclass UsersController < ApplicationController def update @user = Users.new begin ActiveRecord::Base.transaction do @user.assign_attributes(user_params) #ここで一旦中間テーブルに保存されるが、save!でエラーならロールバックされる @user.save! end p "更新成功" rescue p "更新失敗" end end end案2 after_save を利用する
after_saveは、データベースへの COMMIT の直前に実行される
- つまり、model のバリデーションなどを実行後に、そのメソッドを呼び出してくれる
- 自動で生えるリレーション用のフィールドとは別の attribute 名で view からパラメータを送り、model のバリデーション後にそのパラメータの値を保存したい中間テーブルの attribute にセットして保存することで、バリデーション通過後に中間テーブルが保存されるようにする
assign_attributes実行時にはリレーションを意味するフィールドには何もセットせず、after_saveで実行するメソッドの中で、値をセットするようにするusers.rbclass User < ApplicationRecord has_many :group_users has_many :groups, through: :group_users validates :name, presence: true after_save :save_group_user attr_accessor :g_ids # User のバリデーション実行後、コミットする直前に呼ばれる def save_group_user self.group_ids = @g_ids end end
- model には
after_saveを定義し、実行するメソッドの中で中間テーブル用のgroup_idsをセットするようにしておく- また view からきたパラメータをセットしておく用の attribute として
attr_accessor :g_idsを定義しておく- view からは
g_idsのように適当なフィールド名でパラメータを送り、after_saveのメソッドでgroup_idsにセットすることで中間テーブルを保存するusers_controller.rbclass UsersController < ApplicationController def update @user = Users.new @user.assign_attributes(user_params) @user.save ? (redirect_to root_path notice: '更新成功') : (render :edit) end end
- コントローラーでは普通に save を実行して保存すれば良い
ちなみに
- 新規作成時など、User が id フィールドが nil の状態であれば、中間テーブル用のフィールドに値をセットしても自動でインサートが走ることはないので、あくまでも更新時に気をつければいいっぽい
- 投稿日:2020-08-14T19:29:30+09:00
Vision API(トレーニング済み機械学習モデル)を使ってRubyでテキスト抽出(OCR)をためしてみた
Vision APIとは?
Google Cloud の Vision API は REST API や RPC API を介して強力な事前トレーニング済み機械学習モデルを提供します。画像にラベルを割り当てることで、事前定義済みの数百万のカテゴリに画像を高速に分類できます。オブジェクトや顔を検出し、印刷テキストや手書き入力を読み取り、有用なメタデータを画像カタログに作成します。
(公式ドキュメントより引用)分析にかけた画像
アサインナビ トップページのキャプチャ画像(PNG形式)
分析結果
$ bundle exec rake cloud_vision:text['app/assets/images/anavi.png'] assign navi 新しい働き方 案件/人材を探す ご利用ガイド サービスについて ログイン 会員登録 IT案件、外部人材との出会いを テクノロジーで効率化 00かなり高精度で、テキスト抽出できていますね (^^)
今回作成したコード
lib/tasks/cloud_vision.rake## 呼び出し方 ## $ bundle exec rake cloud_vision:text[image_file] # image_fileに'ここに画像ファイルのパス' # Google Cloud client libraryの読み込み require "google/cloud/vision" namespace :cloud_vision do desc 'Run OCR.' task :text, [:image_file] do |task, args| return unless args[:image_file] # クライアントをインスタンス化 image_annotator = Google::Cloud::Vision.image_annotator # OCRの実行 response = image_annotator.text_detection( image: args[:image_file], max_results: 1 # optional, defaults to 10 ) # OCR結果の表示 response.responses.each do |res| res.text_annotations.each do |text| puts text.description end end end end環境変数はdotenvに記載
#.envファイル GOOGLE_CLOUD_PROJECT="ここにプロジェクトIDを記載" GOOGLE_APPLICATION_CREDENTIALS="Vision APIにアクセスする認証キーjsonファイルへのパスを記載"セットアップ手順
セットアップ手順は近日中に追記させて頂きます
参考文献
Using the Vision API with Ruby
フォローや記事へのLGTMは、日々の投稿の励みになります。
温かい1クリックのほど、よろしくお願いします。 m(_ _)m
- 投稿日:2020-08-14T18:18:02+09:00
Invalid route name, already in use: 'new_user_session'のエラー解決方法
環境
Mac OS Catalina
Ruby 2.7.1
Rails 6.0.3.2現象
ログイン機能用にdeviseを導入しrouteを設定。
その後、Rails routesを実行すると下記のエラーが出た。Invalid route name, already in use: 'new_user_session' (ArgumentError) You may have defined two routes with the same name using the `:as` option, or you may be overriding a route already defined by a resource with the same naming. For the latter, you can restrict the routes created with `resources` as explained here: https://guides.rubyonrails.org/routing.html#restricting-the-routes-createdどうやら、'new_user_session'という名前のルートが既に使われているぞ!と言ってる様子。
現象発生時のルートは下記の通り。
routes.rbRails.application.routes.draw do devise_for :users # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html root to: 'toppages#index' resources :users, only: [:index, :show, :edit, :update, :destroy] do member do get :followings get :followers get :likes end end devise_for :users, path: '', path_names: { sign_up: '', sign_in: 'login', sign_out: 'logout', registration: 'signup' }, controllers: { registrations: 'users/registrations', sessions: 'users/sessions', passwords: 'users/passwords' } devise_scope :user do get 'signup', to: 'users/registrations#new' get 'login', to: 'users/sessions#new' get 'logout', to: 'users/sessions#destroy' end resources :posts, only: [:create, :destroy] do collection do get :search end end resources :relationships, only: [:create, :destroy] resources :favorites, only: [:create, :destroy] end解決方法
「2箇所定義とかしてないのにな〜」とか考えていましたが、ふと一番上に行が追加されていることに気づきましたw
devise_for :users の行を削除することで解決。routes.rbRails.application.routes.draw do root to: 'toppages#index' resources :users, only: [:index, :show, :edit, :update, :destroy] do member do get :followings get :followers get :likes end end devise_for :users, path: '', path_names: { sign_up: '', sign_in: 'login', sign_out: 'logout', registration: 'signup' }, controllers: { registrations: 'users/registrations', sessions: 'users/sessions', passwords: 'users/passwords' } devise_scope :user do get 'signup', to: 'users/registrations#new' get 'login', to: 'users/sessions#new' get 'logout', to: 'users/sessions#destroy' end resources :posts, only: [:create, :destroy] do collection do get :search end end resources :relationships, only: [:create, :destroy] resources :favorites, only: [:create, :destroy] end無事、起動することができました!
最後に
already in use系のエラーは、今回と同様に同じルーティングが既に設定されていると考えて良さそうですね。
(そのままですが)
今後同じエラーが出た際は、サクッと解決できそう。
- 投稿日:2020-08-14T17:26:03+09:00
Railsのテンプレート機能(ERB)をController(Action View)以外で使う
概要
Railsは、テンプレートをアクションで紐付けてくれるので便利ですよね。
Action ViewとERB (Embedded Ruby)ってやつです。
たまに、そのテンプレートを、他の箇所で使いたいことがあります。
調べましたがあまり情報が出てこなかったり、見つけたと思ったら動かなかったりしたので解決した方法を備忘として投稿します。動作環境
ruby 2.6.5
rails 5.2.3サンプル
例えばJobとかで利用。
app/views下(任意の場所可)にtemplateを作成。
sample.html.erb<%= user_name %>さんへ これはサンプルです。次にJob。
send_template_job.rbclass SendTemplateJob < ApplicationJob queue_as :default def perform(*args) # テンプレートを使う content = ActionView::Base.new('app/views').render(file: 'sample', locals: { user_name: current_user.name }, layout: false) # テンプレートを利用して生成した文字列をどこかに送る処理を記載 # 例えばslackとか end endこんな感じで書くと、erb内のuser_nameて定義してある変数が文字列として展開され取得できます。
- 投稿日:2020-08-14T16:56:02+09:00
[ruby]ドリルアウトプット
内容
問題.1
名前を入力する機能を作成しましょう。
名前以外にピリオド(.)や空白があるとエラーを表示させます。※わからない場合はAPIを利用して問題を解きましょう。
参考URL: https://docs.ruby-lang.org/ja/search/ヒント
include?メソッドを使いましょう。include?
include?メソッドは指定した要素が、配列や文字列内に含まれているかを判定するメソッドです。
スクリーンショット 2020-08-14 16.50.22.png実装
解説
strに(.)か( )があったら登録できないように、include?で条件分岐させればOKです。
- 投稿日:2020-08-14T16:02:51+09:00
【AWS Lambda】S3に保存された画像をリサイズして別S3に保存(Ruby利用)
目標
S3に保存された画像データをLambda関数を利用してリサイズし、別S3に保存する。
プログラミング言語はRubyを利用しています。はじめに
あまりLambdaに関して詳しくないので、基本的にはLambdaの基本動作を手順として残すのが目的です。
AWSコンソールからS3にプットした画像をリサイズ(固定サイズ)して別S3に保存するという単純な処理を試してみました。前提
事前に2つのS3バケットを作成していること。
構成図
作業の流れ
項番 タイトル 1 Lambda関数の作成とアップロード 2 Lambda環境変数の設定 3 ハンドラーと実行ロールの設定 4 イベントトリガーの定義 5 実行ロールの設定 6 動作検証 手順
1.Lambda関数の作成とアップロード
任意の関数名とLambda関数で利用するプログラミング言語を選択します。
今回はRuby2.5で記述します。
配備するLambda関数本体と外部ライブラリ(mini_magick)をzip化したものを作成します。
作業ディレクトリ上で以下操作を行います。まずは
Gemfileの作成Gemfilesource 'https://rubygems.org' gem "mini_magick"
bundle install --path vendor/bundleを利用して作業ディレクトリ配下にgemをインストールします。bundle install --path vendor/bundle更にLambda関数本体となる
handler.rbを作成します。handler.rbrequire 'aws-sdk-s3' require 'base64' require 'mini_magick' def resize_image(event:, context:) s3_client = Aws::S3::Client.new( :region => ENV['REGION'], :access_key_id => ENV['ACCESS_KEY'], :secret_access_key => ENV['SECRET_ACCESS_KEY'] ) # イベントソースとして指定したS3から保存された画像を取り込む key = event['Records'][0]['s3']['object']['key'] image_file = s3_client.get_object(:bucket => ENV['BUCKET_BEFORE'], :key => key).body.read image = MiniMagick::Image.read(image_file) # リサイズした画像をLambda環境の/tmpに一時書き込み resized_tmp_file = "/tmp/#{key.delete("images/")}" image.resize("300x300").write(resized_tmp_file) # アップロード実行 s3_resource = Aws::S3::Resource.new() object = s3_resource.bucket(ENV['BUCKET_AFTER']).object(key).upload_file(resized_tmp_file) end
handler.rbとvenderをzip化します。作成したzipファイルのアップロードを行います。
Lambdaコンソール上の関数コード欄アクションから.zipファイルをアップロードをクリック
handler.zipを選択し、保存2.Lambda環境変数の設定
Lambdaコンソール上からLambda関数内で利用する環境変数を設定します。
3.ハンドラーと実行ロールの設定
どのファイルのどの関数を呼び出すか…という設定は、ハンドラーというパラメータで指示しているとのことなので、
今回作成したLambda関数に合わせて内容を編集します。ハンドラ欄に
実行ファイル名(拡張子除く).関数名の形式で値を入力し保存します。
4.イベントトリガーの定義
トリガーの詳細設定を行います。
今回はimagesディレクトリ内にオブジェクトが作成されたタイミングでのイベント起動とします。
トリガーの有効化をいれることでS3がこのLambda関数に処理をキックすることが可能となります(Lambda関数ポリシーの設定)。
最後に追加をクリック5.実行ロールの設定
更に今回はリサイズした画像を保存する際にLambdaからS3にアクセスをかける必要があるため、
S3へのアクセス権限付きの実行ロールを付与します。Lambdaコンソール上部にある
アクセス権限をクリック後、実行ロール名をクリック
AmazonS3FullAccessを選択後、ポリシーのアタッチ
6.動作検証
イベントソースとして指定したS3のimagesディレクトリ内に任意の画像をアップロードします。
リサイズ後の画像保存先として指定したS3を確認したところ、画像が保存されていたのでOKです。
LambdaのログはCloudwatch Logsから確認可能です。
参考にさせて頂いた記事
- 投稿日:2020-08-14T16:02:51+09:00
【AWS Lambda】S3に保存された画像をリサイズして別S3に保存
目標
S3に保存された画像データをLambda関数を利用してリサイズし、別S3に保存する。
はじめに
あまりLambdaに関して詳しくないので、基本的にはLambdaの基本動作を手順として残すのが目的です。
AWSコンソールからS3にプットした画像をリサイズ(固定サイズ)して別S3に保存するという単純な処理を試してみました。前提
事前に2つのS3バケットを作成していること。
構成図
作業の流れ
項番 タイトル 1 Lambda関数の作成とアップロード 2 Lambda環境変数の設定 3 ハンドラーと実行ロールの設定 4 イベントトリガーの定義 5 実行ロールの設定 6 動作検証 手順
1.Lambda関数の作成とアップロード
任意の関数名とLambda関数で利用するプログラミング言語を選択します。
今回はRuby2.5で記述します。
配備するLambda関数本体と外部ライブラリ(mini_magick)をzip化したものを作成します。
作業ディレクトリ上で以下操作を行います。まずは
Gemfileの作成Gemfilesource 'https://rubygems.org' gem "mini_magick"
bundle install --path vendor/bundleを利用して作業ディレクトリ配下にgemをインストールします。bundle install --path vendor/bundle更にLambda関数本体となる
handler.rbを作成します。handler.rbrequire 'aws-sdk-s3' require 'base64' require 'mini_magick' def resize_image(event:, context:) s3_client = Aws::S3::Client.new( :region => ENV['REGION'], :access_key_id => ENV['ACCESS_KEY'], :secret_access_key => ENV['SECRET_ACCESS_KEY'] ) # イベントソースとして指定したS3から保存された画像を取り込む key = event['Records'][0]['s3']['object']['key'] image_file = s3_client.get_object(:bucket => ENV['BUCKET_BEFORE'], :key => key).body.read image = MiniMagick::Image.read(image_file) # リサイズした画像をLambda環境の/tmpに一時書き込み resized_tmp_file = "/tmp/#{key.delete("images/")}" image.resize("300x300").write(resized_tmp_file) # アップロード実行 s3_resource = Aws::S3::Resource.new() object = s3_resource.bucket(ENV['BUCKET_AFTER']).object(key).upload_file(resized_tmp_file) end
handler.rbとvenderをzip化します。作成したzipファイルのアップロードを行います。
Lambdaコンソール上の関数コード欄アクションから.zipファイルをアップロードをクリック
handler.zipを選択し、保存2.Lambda環境変数の設定
Lambdaコンソール上からLambda関数内で利用する環境変数を設定します。
3.ハンドラーと実行ロールの設定
どのファイルのどの関数を呼び出すか…という設定は、ハンドラーというパラメータで指示しているとのことなので、
今回作成したLambda関数に合わせて内容を編集します。ハンドラ欄に
実行ファイル名(拡張子除く).関数名の形式で値を入力し保存します。
4.イベントトリガーの定義
トリガーの詳細設定を行います。
今回はimagesディレクトリ内にオブジェクトが作成されたタイミングでのイベント起動とします。
トリガーの有効化をいれることでS3がこのLambda関数に処理をキックすることが可能となります(Lambda関数ポリシーの設定)。
最後に追加をクリック5.実行ロールの設定
更に今回はリサイズした画像を保存する際にLambdaからS3にアクセスをかける必要があるため、
S3へのアクセス権限付きの実行ロールを付与します。Lambdaコンソール上部にある
アクセス権限をクリック後、実行ロール名をクリック
AmazonS3FullAccessを選択後、ポリシーのアタッチ
6.動作検証
イベントソースとして指定したS3のimagesディレクトリ内に任意の画像をアップロードします。
リサイズ後の画像保存先として指定したS3を確認したところ、画像が保存されていたのでOKです。
LambdaのログはCloudwatch Logsから確認可能です。
参考にさせて頂いた記事
- 投稿日:2020-08-14T12:21:48+09:00
Ruby 二次元配列とは?
二次元配列とは?
二次元配列とは、配列を要素に持つ配列のこと。
# 例: number_arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] color_arrays = [["red", "yellow", "blue"], ["white", "black", "gray"]]二次元配列を使用することで、二次元(平面)のデータを表すことができる。
例:マス目の状態(◯ or ✕)を二次元配列で表現する# ◯:1, ✕:0 とする arrays = [ [1, 1, 0, 0], [1, 1, 1, 1], [0, 1, 1, 1], [1, 1, 0, 1], [1, 0, 1, 1] ]二次元配列の使い方
二次元配列の作成
二次元配列は通常の配列と同じように作成できる。
# 直接定義して作成 arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] # 空の配列arraysを定義し、その中に配列を追加して作成 arrays = [] arrays << [1, 2, 3] arrays << [4, 5, 6] arrays << [7, 8, 9] arrays #=> [[1, 2, 3], [4, 5, 6], [7, 8, 9]]値の取得
二次元配列内の配列を取得するには
変数名[取得する配列のインデックス番号]
のように記述する。arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] arrays[0] #=> [1, 2, 3] arrays[1] #=> [4, 5, 6] arrays[2] #=> [7, 8, 9]また、二次元配列内の配列の各要素を取得するには
変数名[取得する配列のインデックス番号][取得する要素のインデックス番号]
のように記述する。arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] arrays[0][0] #=> 1 arrays[0][1] #=> 2 arrays[0][2] #=> 3 arrays[1][0] #=> 4 arrays[1][1] #=> 5 arrays[1][2] #=> 6 arrays[2][0] #=> 7 arrays[2][1] #=> 8 arrays[2][2] #=> 9二次元配列用のメソッド
二次元配列用のメソッドもある。
transpose
transposeメソッドを使用すると、二次元配列の行と列を入れ替えた二次元配列を作成できる。
# 例: arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] # arraysを平面で表すと以下のようになる # arrays = [ # [1, 2, 3], # [4, 5, 6], # [7, 8, 9] # ] transposed_arrays = arrays.transpose transposed_arrays #=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]] # transposed_arraysを平面で表すと以下のようになる # transposed_arrays = [ # [1, 4, 7], # [2, 5, 8], # [3, 6, 9] # ]flatten
flattenメソッドを使うと、二次元配列を平坦化(中の配列をバラす)した配列を作成できる。
# 例: arrays = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flattened_arrays = arrays.flatten flattened_arrays #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]まとめ
- 二次元配列とは、配列を要素に持つ配列のこと
- 二次元配列を使うと平面のデータを表現できる
- 二次元配列内の配列や値はインデックス番号を指定することで取得できる
- transpose, flattenなどの二次元配列に使用できるメソッドもある
- 投稿日:2020-08-14T12:15:15+09:00
Ruby 文字列の中から文字を抽出 sliceメソッド
sliceメソッドとは?
sliceメソッドとは、文字列の中から引数で指定した条件の文字を抜き出すメソッド。
# 例:文字列 "Hello world" の頭から2文字目の "e" を抜き出す string = "Hello world" string.slice(1) #=> "e" # 引数に該当する文字が見つからなければnilを返す string.slice(11) #=> nilsliceメソッドの使い方
sliceメソッドは引数に、抜き出す文字の条件指定する。
引数の指定は以下の様な方法がある。文字列の先頭 or 後ろから何文字目かを指定
文字列の先頭から何文字目かを指定する。番号は0から始まるのに注意(先頭の文字が0、先頭から2番目が1)
string = "Hello world" # 先頭から何文字目かを指定、先頭の番号は0 string.slice(0) #=> "H" string.slice(1) #=> "e" string.slice(2) #=> "l" string.slice(3) #=> "l" string.slice(4) #=> "o"マイナスの値を指定することで、後ろから何文字目かを指定することもできる。その場合、一番後ろの文字が-1、後ろから2番目が-2のように数えられる。
string = "Hello world" # 後ろから何文字目かを指定、一番後ろの番号は-1 string.slice(-5) #=> "w" string.slice(-4) #=> "o" string.slice(-3) #=> "r" string.slice(-2) #=> "l" string.slice(-1) #=> "d"文字列の先頭 or 後ろから何番目かと、そこから何文字かを指定
文字列の中の範囲を指定して文字を抽出する。始まりの位置と、そこから何文字抽出するかを指定する。この場合も、番号は0から始まる。
string = "Hello world" # 先頭(0番目)から5文字抽出 string.slice(0,5) #=> "Hello" # 6番目から5文字抽出 string.slice(6,5) #=> "world"始まりの位置にマイナスの値を指定することで、後ろから何文字目かを指定することもできる。この場合も一番後ろの文字が-1、後ろから2番目が-2のように数えられる。
string = "Hello world" # -11番目から5文字抽出 string.slice(-11,5) #=> "Hello" # -5番目から5文字抽出 string.slice(-5,5) #=> "world"抽出したい文字を直接指定
特定の文字を指定して抽出する。
string = "Hello world" # "H"を指定 string.slice("H") #=> "H" # "Hello"を指定 string.slice("Hello") #=> "Hello" # 含まれていない文字を指定するとnilを返す string.slice("hello") #=> nil正規表現で指定
抽出したい文字を正規表現で指定する。
string = "Hello world" # 正規表現で「wから始まってdで終わる文字」を指定 string.slice(/w.*d/) #=> "world"範囲演算子で範囲を指定
範囲演算子( .. , ... )を使って文字列内の範囲を指定し、文字を抽出する。この場合も、番号は0から始まる。
string = "Hello world" # 先頭(0番目)から4番目まで抽出 string.slice(0..4) #=> "Hello" # ...にすると範囲の終わりの文字を含まない string.slice(0...4) #=> "Hell"sliceメソッドとslice!メソッドの違い
slice!というメソッドもあるが、こちらもsliceメソッドと同じように文字列の中から文字を抽出して返すメソッドである。引数の指定方法もsliceメソッドと同じ。ただし、slice!メソッドを使うと、抽出した文字は元の文字列から取り除かれる。
string = "Hello world" # sliceメソッドを使用しても元のstringは変わらない string.slice(0..4) #=> "Hello" string #=> "Hello world" # slice!メソッドを使用すると元のstringから文字列が取り除かれる string.slice!(0..4) #=> "Hello" string #=> " world"まとめ
- sliceメソッドは文字列から文字を抽出するメソッド
- 引数に指定した条件で文字を抽出する
- 引数は文字の順番や正規表現、範囲で指定できる
- slice!メソッドは元の文字列から文字を取り除く
- 投稿日:2020-08-14T11:47:54+09:00
【Rails】Active Recordのjoinsメソッドで連結したテーブルから一意なレコードを取得する(Rails Tutorial第14章)
はじめに
Railsチュートリアルに取り込む中で教材で触れられていないバグに遭遇した。
バグの原因追及と解決方法をまとめる。この記事で分かること
・Active Recordの『joins』メソッドによる連結されたテーブルのカラム名の確認方法
・重複したレコードを一意にして取得する『distinct』メソッドの使用方法参考資料
・Railsチュートリアル
https://railstutorial.jp/
・Railsガイド Active Record の関連付け
https://railsguides.jp/association_basics.html
・【Rails】データベースの中身を確認する方法【Cloud9】
https://shuheitakada.com/rails-database-check
・SELECT文の結果を表示する時にカラム名をヘッダーとして表示(.headersコマンド)
https://www.dbonline.jp/sqlite/sqlite_command/index5.html
・Rails distinctメソッドについて
https://qiita.com/toda-axiaworks/items/ad5a0e2322ac6a2ea0f4環境について
Railsチュートリアル第6版に従う
・cloud9使用
・ruby 2.6.3p62
・Rails 6.0.3学習動機
Rails Tutorial第14章 14.3.3サブセレクト 演習3にてバグが生じる。
演習の目的はjoinsメソッドを使用してフォローしたユーザーと自分自身に紐づくマイクロポストをフィードとして表示させることである。
一見いい感じに見えるが、
どう考えてもenjoyしすぎである。
明らかにenjoyしてない気持ちを抑えつつ原因の究明をする。モデルの関連付
各モデルの関連付は以下の通り。
class User < ApplicationRecord has_many :microposts has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id" has_many :followers, through: :passive_relationships, source: :follower end class Relationship < ApplicationRecord belongs_to :follower, class_name: "User" end class Micropost < ApplicationRecord belongs_to :user end # Copyright (c) 2016 Michael Hartl原因となったコード
models/user.rbdef feed part_of_feed = "relationships.follower_id = :id or microposts.user_id = :id" Micropost.joins(user: :followers).where(part_of_feed, { id: id }) end # Copyright (c) 2016 Michael Hartlまずは、コンソールに入り、Railsにより作成されるSQL 文を確認する。
rails_console>>user.feed SELECT * FROM "microposts" INNER JOIN "users" ON "users"."id" = "microposts"."user_id" INNER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" INNER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id" WHERE (relationships.follower_id = 1 or microposts.user_id = 1) ORDER BY "microposts"."created_at" DESC;次に、データベースのコンソールにてjoinsメソッドにより作成されたテーブルの構造とデータを確認する。
rails_dbconsole#SELECTの結果でカラム名を表示させる >>.header on >>SELECT * FROM "microposts" INNER JOIN "users" ON "users"."id" = "microposts"."user_id" INNER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" INNER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id" WHERE (relationships.follower_id = 1 or microposts.user_id = 1) ORDER BY "microposts"."created_at" DESC LIMIT 5; #検索結果 |~|はカラムの省略の意 id|content|user_id|~|id|name|~|id|follower_id|followed_id|~|id|name|~| 308|Enjoy Coding !!|1|~|1|Example User|~|50|4|1|~|4|Mr. Rey Lemke|~| 308|Enjoy Coding !!|1|~|1|Example User|~|51|5|1|~|5|Dr. Louisa Price|~| 308|Enjoy Coding !!|1|~|1|Example User|~|52|6|1|~|6|Charisse Stamm|~| 308|Enjoy Coding !!|1|~|1|Example User|~|53|7|1|~|7|Sang Metz IV|~| 308|Enjoy Coding !!|1|~|1|Example User|~|54|8|1|~|8|Robt Hamill|~|どうやら、follower_idによりMicropostsにダブりが生じていることが分かる。
ちなみにMicropostのダブりの数と自身のFollowersの数が一致していることからも、
Followersがダブりの原因になっていると推測できる。つまり、下の様なフォロー関係の場合、
id:1 田中 → id:2 鈴木
id:2 鈴木 → id:1 田中
id:3 佐藤 → id:1 田中
id:3 佐藤 → id:2 鈴木
下記図の様なテーブルが作成される。
Micopost.whereにより、みどり枠とあか枠の部分が抽出され、あか枠部分がダブりとなっている。
models/user.rb#みどり枠部分 relationships.follower_id = :id: #あか枠部分 microposts.user_id = :id解決策
原因が分かるまで結構な時間を使ってしまったけれど、解決方法が分かるとたったの1語で解決できる。
distinctメソッドを使う
重複のない一意性のあるレコードを取得するためには、『distinct』メソッドを使用するとのこと。
早速組み込んでみる。
models/user.rbdef feed part_of_feed = "relationships.follower_id = :id or microposts.user_id = :id" Micropost.joins(user: :followers).where(part_of_feed, { id: id }).distinct end #Copyright (c) 2016 Michael Hartlテストを書く
今こそテストを書く時。
ということでFeedに自分が投稿したマイクロポストの重複がないことをテストする。test/integrationtest/microposts_interface_test.rbdef setup @user = users(:michael) end test "should feed have microposts with uniqueness" do log_in_as(@user) get root_path # マイクロポストの投稿 content = "This micropost is only one!" post microposts_path, params: { micropost: { content: content }} follow_redirect! # feedのダブりの確認 assert_select 'span.content', { :count=>1, :text=> "#{content}" } end #Copyright (c) 2016 Michael Hartl
- 投稿日:2020-08-14T11:01:49+09:00
CarrierWaveを使い、複数の画像を一斉に投入する2つの方法/①seedの初期データで投入/②CSVでインポートする
他のtextカラムは簡単に複数投入できるのですが、image(画像)カラムだけはいずれもnilになってしまい苦労したので、ここに複数の画像カラムを一斉に投入する2つの方法を記しておきます。
①複数の画像カラムをseedの初期データで投入する方法
db/migrate/seed.rbに、seed.rbPost.create!(image: /public/uploads/post/image/1/yatoguti.jpg, park: 谷戸口公園, outline: 程久保の知る人ぞ知る, location: 東京都日野市程久保1丁目20−14, access: モノレール程久保駅から徒歩5分)のように記載し、
rails db:resetしてから、rails db:seedするが、seed.rb=> [#<Post:0x00007fa6c66bc8e8 id: 27, image: nil, park: "谷戸口公園", outline: "程久保の知る人ぞ知る。", location: "京王線高幡不動駅より百草園駅方面へ徒歩10分", access: "日野市高幡1024番地", created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, likes_count: nil, tag_list: ["#水遊び", "#アスレチック"]>, #<Post:0x00007fa6c7b48f28 id: 28, image: nil, park: "雨乞公園", outline: "明るさと落ち着いた雰囲気の両面を持つ公園です。", location: "日野市百草881番地の8", access: "三沢台小学校バス停より東へ徒歩3分", created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, likes_count: nil, :...skipping...このように、画像カラムは直接URLだけをを記載するとnilになってしまう。
解決方法
imageカラムの画像URLの記載を下記のように書き換える。
seed.rbPost.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"), park: 谷戸口公園, outline: 程久保の知る人ぞ知る, location: 東京都日野市程久保1丁目20−14, access: モノレール程久保駅から徒歩5分)imageカラムの記載を、
Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,ではなく、
Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),に変更する。そして、
rails db:resetしてから、rails db:seedする。※
/image/1/yatoguti.jpgには、実際に画像が入っているかどうか、予めファイルを確認しておいて下さい。これで、seed初期データの一斉投入が出来ました!
②複数の画像カラムをCSVでインポートする方法
seed.rbで初期データ投入では成功したので、今度は
db/csv_dara/csv_data.csvとlib/tasks/import_csv.rakeで、同じように記載して、CSVインポートでも試してみる。csv_data.csvimage,park,outline,location,access,tag_list File.open("#{Rails.root}/public/uploads/post/image/6/ajisai.jpg"),芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場 File.open("#{Rails.root}/public/uploads/post/image/8/hohoemi.jpg"),ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ File.open("#{Rails.root}/public/uploads/post/image/7/hodokubo.jpg"),ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生lib/tasks/import_csv.rakerequire 'csv' namespace :import_csv do desc "postテーブルのCSVデータをインポートするタスク" task posts: :environment do path = File.join Rails.root, "db/csv_data/csv_data.csv" list = [] CSV.foreach(path, headers: true) do |row| list << { image: row ["image"], park: row["park"], outline: row["outline"], location: row["location"], access: row["access"], tag_list: row["tag_list"] } end puts "インポート処理を開始" Post.create!(list) puts "インポート完了!!" end endseedの初期投入ができた記載だから、CSVインポートも出来ると思っていました。すると…
rake aborted! CSV::MalformedCSVError: Illegal quoting in line 2. /Users/sekishinya/Desktop/park_app/lib/tasks/import_csv.rake:11:in `block (2 levels) in <main>' Tasks: TOP => import_csv:posts (See full trace by running task with --trace)エラーでインポートできません。
元々imageカラム抜きでインポートしていたので、imageカラムが問題なのは間違いないはずですが。解決方法
CSVは、seedの時とは異なり、全て文字列扱いとなるので記載が異なる。
そこで、
lib/tasks/import_csv.rakeと、db/csv_dara/csv_data.csvを、以下のように書き直す。import_csv.rakerequire 'csv' namespace :import_csv do desc "postテーブルのCSVデータをインポートするタスク" task posts: :environment do path = File.join Rails.root, "db/csv_data/csv_data.csv" list = [] CSV.foreach(path, headers: true) do |row| list << { image: File.open("#{Rails.root}/#{row["image"]}"), park: row["park"], outline: row["outline"], location: row["location"], access: row["access"], tag_list: row["tag_list"] } end puts "インポート処理を開始" Post.create!(list) puts "インポート完了!!" end end
import_csv.rakeのimageカラムの記載を、image: row ["image"],ではなく、
image: File.open("#{Rails.root}/#{row["image"]}"),に変更する。
そして、
csv_data.csvのimageカラムを以下のように変更する。csv_data.csvimage,park,outline,location,access,tag_list "public/uploads/post/image/8/fuyou.jpg",芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場 "public/uploads/post/image/8/hohoemi.jpg",ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ "public/uploads/post/image/8/hodokubo.jpg",ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生seedの時は、URLのみの記載で失敗しましたが、逆にCSVデータはURLのみにした方が良いですね。
これで見事に、複数の画像の一斉インポートに成功しました!
- 投稿日:2020-08-14T11:01:49+09:00
【Rails6】CarrierWaveを使い、複数の画像を一斉に投入する2つの方法/①seedの初期データで投入/②CSVでインポートする
他のtextカラムは簡単に複数投入できるのですが、image(画像)カラムだけはいずれもnilになってしまい苦労したので、ここに複数の画像カラムを一斉に投入する2つの方法を記しておきます。
環境
- Ruby 2.6.5
- Rails 6.0.3.2
①複数の画像カラムをseedの初期データで投入する方法
db/migrate/seed.rbに、seed.rbPost.create!(image: /public/uploads/post/image/1/yatoguti.jpg, park: 谷戸口公園, outline: 程久保の知る人ぞ知る, location: 東京都日野市程久保1丁目20−14, access: モノレール程久保駅から徒歩5分)のように記載し、
rails db:resetしてから、rails db:seedするが、seed.rb=> [#<Post:0x00007fa6c66bc8e8 id: 27, image: nil, park: "谷戸口公園", outline: "程久保の知る人ぞ知る。", location: "京王線高幡不動駅より百草園駅方面へ徒歩10分", access: "日野市高幡1024番地", created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, likes_count: nil, tag_list: ["#水遊び", "#アスレチック"]>, #<Post:0x00007fa6c7b48f28 id: 28, image: nil, park: "雨乞公園", outline: "明るさと落ち着いた雰囲気の両面を持つ公園です。", location: "日野市百草881番地の8", access: "三沢台小学校バス停より東へ徒歩3分", created_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, updated_at: Sun, 02 Aug 2020 14:38:30 JST +09:00, likes_count: nil, :...skipping...このように、画像カラムは直接URLだけをを記載するとnilになってしまう。
解決方法
imageカラムの画像URLの記載を下記のように書き換える。
seed.rbPost.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"), park: 谷戸口公園, outline: 程久保の知る人ぞ知る, location: 東京都日野市程久保1丁目20−14, access: モノレール程久保駅から徒歩5分)imageカラムの記載を、
Post.create!(image: /public/uploads/post/image/1/yatoguti.jpg,ではなく、
Post.create!(image: File.open("#{Rails.root}/public/uploads/post/image/1/yatoguti.jpg"),に変更する。そして、
rails db:resetしてから、rails db:seedする。※
/image/1/yatoguti.jpgには、実際に画像が入っているかどうか、予めファイルを確認しておいて下さい。これで、seed初期データの一斉投入が出来ました!
②複数の画像カラムをCSVでインポートする方法
seed.rbで初期データ投入では成功したので、今度は
db/csv_dara/csv_data.csvとlib/tasks/import_csv.rakeで、同じように記載して、CSVインポートでも試してみる。csv_data.csvimage,park,outline,location,access,tag_list File.open("#{Rails.root}/public/uploads/post/image/6/ajisai.jpg"),芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場 File.open("#{Rails.root}/public/uploads/post/image/8/hohoemi.jpg"),ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ File.open("#{Rails.root}/public/uploads/post/image/7/hodokubo.jpg"),ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生lib/tasks/import_csv.rakerequire 'csv' namespace :import_csv do desc "postテーブルのCSVデータをインポートするタスク" task posts: :environment do path = File.join Rails.root, "db/csv_data/csv_data.csv" list = [] CSV.foreach(path, headers: true) do |row| list << { image: row ["image"], park: row["park"], outline: row["outline"], location: row["location"], access: row["access"], tag_list: row["tag_list"] } end puts "インポート処理を開始" Post.create!(list) puts "インポート完了!!" end endseedの初期投入ができた記載だから、CSVインポートも出来ると思っていました。すると…
rake aborted! CSV::MalformedCSVError: Illegal quoting in line 2. /Users/sekishinya/Desktop/park_app/lib/tasks/import_csv.rake:11:in `block (2 levels) in <main>' Tasks: TOP => import_csv:posts (See full trace by running task with --trace)エラーでインポートできません。
元々imageカラム抜きでインポートしていたので、imageカラムが問題なのは間違いないはずですが。解決方法
CSVは、seedの時とは異なり、全て文字列扱いとなるので記載が異なる。
そこで、
lib/tasks/import_csv.rakeと、db/csv_dara/csv_data.csvを、以下のように書き直す。import_csv.rakerequire 'csv' namespace :import_csv do desc "postテーブルのCSVデータをインポートするタスク" task posts: :environment do path = File.join Rails.root, "db/csv_data/csv_data.csv" list = [] CSV.foreach(path, headers: true) do |row| list << { image: File.open("#{Rails.root}/#{row["image"]}"), park: row["park"], outline: row["outline"], location: row["location"], access: row["access"], tag_list: row["tag_list"] } end puts "インポート処理を開始" Post.create!(list) puts "インポート完了!!" end end
import_csv.rakeのimageカラムの記載を、image: row ["image"],ではなく、
image: File.open("#{Rails.root}/#{row["image"]}"),に変更する。
そして、
csv_data.csvのimageカラムを以下のように変更する。csv_data.csvimage,park,outline,location,access,tag_list "public/uploads/post/image/8/fuyou.jpg",芙蓉公園,日野市の「高幡不動」のすぐそばにあるこぢんまりとした公園です。団地の中の高台にあり、地域の子供達の憩いの場となっています。ブランコ、すべり台、鉄棒、砂場があり、自由に遊ぶことができるスペースも確保されています。,東京都日野市高幡714-21,高幡不動駅から徒歩5分,#滑り台 #鉄棒 #ブランコ #砂場 "public/uploads/post/image/8/hohoemi.jpg",ほほえみ公園,コンクリートの小山が人気の公園です。小山の中には縦横に走るトンネル、外には留め金のついた登山ルート、そして幅広いすべり台と砂場とが一体化していて、ちょっとした冒険気分が楽しめます。,日野市南平2丁目31番地の6,北野街道口バス停より東へ5分,#滑り台 #鉄棒 #ブランコ "public/uploads/post/image/8/hodokubo.jpg",ほどくぼ地区広場,少し傾斜のある草原と雑木林。遊具はありませんが、木登り、草すべり、どんぐり拾いなどが楽しめます。原っぱなので、はだしでかけまわっても大丈夫。,日野市程久保3丁目22番地の2,京王線多摩動物公園駅より東へ徒歩5分,#芝生seedの時は、URLのみの記載で失敗しましたが、逆にCSVデータはURLのみにした方が良いですね。
これで見事に、CSVでも複数の画像の一斉インポートに成功しました!
- 投稿日:2020-08-14T08:49:49+09:00
カリー化チートシート【記述例一覧版】
拙作記事『カリー化チートシート』について,対応言語や補足追記が増えるにつれ,チートシートというよりは解説記事と化してきたので,あらためて記述例一覧版を作成.しばらくしたら,元記事タイトルを『カリー化記述まとめ』に変えて,こちらを『カリー化チートシート』にするかも.
記述例一覧
複数の記法が可能な言語は,最新バージョン&カリー化メソッド未使用の場合の,最も短い書き方のみを掲載.その他の記法やバージョン等による違いは元記事を参照.
言語 (λxy.(真, if x>y; and 偽, if x≦y)) 10 20 Haskell (\x y -> x > y) 10 20Scheme (((lambda (x) (lambda (y) (> x y))) 10) 20)Python (lambda x: lambda y: x > y)(10)(20)Ruby -> x { -> y { x > y } }[10][20]JavaScript (x => y => x > y)(10)(20)Scala ((x: Int) => (y: Int) => x > y)(10)(20)Perl sub { my $x = shift; return sub { my $y = shift; return $x > $y }; }->(10)->(20)Go言語 func(x int) func(int) bool { return func(y int) bool { return (x > y) } }(10)(20)PHP (fn($x) => fn($y) => $x > $y)(10)(20)Julia (x -> y -> x > y)(10)(20)Emacs Lisp, Common Lisp (funcall (funcall (lambda (x) (lambda (y) (> x y))) 10) 20)R言語 (function(x) { function(y) { x > y } })(10)(20)更新履歴
- 2020-08-14:大きい方の値ではなく真偽値を返す記述例に変更
- 2020-08-14:初版公開(Haskell,Scheme,Python,Ruby,JavaScript,Scala,Perl,Go言語,PHP,Julia,Emacs Lisp/Common Lisp,R言語)
- 投稿日:2020-08-14T03:36:57+09:00
RailsでAjaxでいいね機能を実装する方法
「Railsでいいね機能を実装する方法」でいいね機能の実装方法をご紹介しましたが、今回はそのいいね機能をAjax(非同期通信)実装する方法をご紹介いたします。
完成系は以下のような感じです。
環境
- Ruby 2.5.7
- Rails 5.2.4
前提
- この記事によって、いいね機能が実装済みであること
index.html.erbを編集
2つのlink_to(method: :delete と method: post)に remote: trueを追記します。
remote: trueを記載することで、Ajaxでの処理を実行することができます。index.html.erb<div class="container"> <h1>記事一覧</h1> <table class="table"> <% @posts.each do |post| %> <tr> <td><%= post.title %></td> <td> <% if post.liked_by?(current_user) %> <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %> <%= link_to like_path(like), method: :delete, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;"> <span><%= post.likes.count %></span> <% end %> <% else %> <%= link_to post_likes_path(post), method: :post, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;"> <span><%= post.likes.count %></span> <% end %> <% end %> </td> </tr> <% end %> </table> </div>いいね機能の部分をテンプレート化する
index.html.erbと同じディレクトリに以下のファイルを作成し、いいね機能の部分をコピーし、貼り付けます。
_like.html.erb<% if post.liked_by?(current_user) %> <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %> <%= link_to like_path(like), method: :delete, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;"> <span><%= post.likes.count %></span> <% end %> <% else %> <%= link_to post_likes_path(post), method: :post, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;"> <span><%= post.likes.count %></span> <% end %> <% end %>部分テンプレート(_like.html.erb)を呼び出すため、いいね機能の部分があったところにrenderを記述します。
また、Ajaxの処理がされる部分を識別できるように id を記述します。index.html.erb<div class="container"> <h1>記事一覧</h1> <table class="table"> <% @posts.each do |post| %> <tr> <td><%= post.title %></td> <td id="like-<%= post.id %>"> <!--idで識別できるようにする--> <%= render "like", post: post %> <!--renderで部分テンプレートを呼び出す--> </td> </tr> <% end %> </table> </div>controllerの編集
各アクションの最後にredirect_backをしていましたが、redirect_backをすると再読み込みをしていまい、Ajaxが機能しません。
そのため、redirect_backを削除します。likes_controller.rbdef create like = Like.new(user_id: current_user.id, post_id: params[:post_id]) @post = like.post like.save end def destroy like = Like.find(params[:id]) @post = like.post like.destroy endjsファイルの作成
remote: trueによってjs形式のリクエストを送信しているため、実行するアクション名(createやdestroy)のjsファイルを最終的に探しに行きます。
そのため、app/views/配下にlikesフォルダを作成し、そのフォルダの中に create.js.erb と destory.js.erb を作成します。create.js.erb$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");destory.js.erb$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");idで識別し部分的にhtmlを書き換えます。
これで完成です。
- 投稿日:2020-08-14T03:23:59+09:00
Railsでいいね機能を実装する方法
Railsでいいね機能を実装する方法をご紹介いたします。
完成系は以下のような感じです。
環境
- Ruby 2.5.7
- Rails 5.2.4
前提
- deviseによるログイン機能が実装できていること
- ユーザーのテーブルは"users"、記事のテーブルは"posts"、いいねの中間テーブルは"likes"とする。
- postsのカラムはtitleを追加する
Likeモデルを追加
$ rails g model like user_id:integer post_id:integer $ rails db:migrateアソシエーションを設定
それぞれ以下を記述し、アソシエーションを設定します。
like.rbbelongs_to :user belongs_to :postuser.rbhas_many :likes, dependent: :destroypost.rbhas_many :likes, dependent: :destroyルーティングを設定する
routes.rbresources :posts, shallow: true do resources :likes, only: [:create, :destroy] endliked_by?メソッドを作成
post.rbdef liked_by?(user) likes.where(user_id: user.id).exists? endcontrollerの記述
likes_controller.rbdef create like = Like.new(user_id: current_user.id, post_id: params[:post_id]) @post = like.post like.save redirect_back(fallback_location: posts_path) end def destroy like = Like.find(params[:id]) @post = like.post like.destroy redirect_back(fallback_location: posts_path) endBootstrapを導入
Gemfilegem 'bootstrap-sass', '~> 3.3.6' gem 'jquery-rails'$ bundle installapplication.cssのファイル名をapplication.scssに変更する
$ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scssBootstrapをscssに読み込ませる
application.scssの一番下に以下を記述
application.scss@import "bootstrap-sprockets"; @import "bootstrap";application.jsを編集する
application.jsの以下の部分を
application.js//= require rails-ujs //= require turbolinks //= require_tree .から
application.js//= require rails-ujs //= require jquery //= require bootstrap-sprockets //= require_tree .に書き換えます。
index.html.erbを編集
index.html.erb<div class="container"> <h1>記事一覧</h1> <table class="table"> <% @posts.each do |post| %> <tr> <td><%= post.title %></td> <td> <% if post.liked_by?(current_user) %> <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %> <%= link_to like_path(like), method: :delete do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;"> <span><%= post.likes.count %></span> <% end %> <% else %> <%= link_to post_likes_path(post), method: :post do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;"> <span><%= post.likes.count %></span> <% end %> <% end %> </td> </tr> <% end %> </table> </div>これで完成です。
参考記事
- 投稿日:2020-08-14T02:26:03+09:00
初心者がDeviseのコードを見て仕組みをふんわり理解する【registrations new】
はじめに
Railsチュートリアルがやっと1周終わった者です。
gemのDeviseを入れてみたら、Railsチュートリアルでの5~6章分の実装が10分ほどで終わり、驚愕しています。なんでできたのか分からない...
そして、テストも書きたいけどどうやって書いたらいいのか分からない。
そこで「公式やソースコードを読みましょう」ということで、読んでみたけどこれまたさっぱり分からない...ながらも少しずつ理解したいので、読み込んでいきます。読みながら自分が調べたことをまとめていきます。
・初心者だけどDeviseの仕組みを知りたい
そんな方の役に立てたら幸いです。
DeviseのGitHub
https://github.com/heartcombo/deviseDeviseの概要
GitHubのREADMEの一番最初に、概要の説明があります。
- Deviseは、Wardenに基づくRails向けの柔軟な認証ソリューションです。
- ラックベースです。
- Railsエンジンに基づく完全なMVCソリューションです。
- 複数のモデルに同時にサインインさせることができます。
- モジュール性の概念に基づいています。本当に必要なものだけを使用してください。
ここで出てくるWardenというのは認証のためのgemで、devise内でこれを引っ張ってきているようです。
また、10個のモジュールで構成されており、必要なものはコメントアウトを外したりしながら使ってね、ということのようです。
モジュールというかもう機能ですね。
Qiita内で表にしてくださっている方がいらっしゃったので、下記にて引用します。
機能 概要 database_authenticatable サインイン時にユーザーの正当性を検証するためにパスワードをハッシュ化してDBに登録します。認証方法としてはPOSTリクエストかHTTP Basic認証が使えます。 registerable 登録処理を通してユーザーをサインアップします。また、ユーザーに自身のアカウントを編集したり削除できるようにします。 recoverable パスワードをリセットし、それを通知します。 rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除します。 trackable サインイン回数や、サインイン時間、IPアドレスを記録します。 validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます。 confirmable メールに記載されているURLをクリックして本登録を完了する、といったよくある登録方式を提供します。また、サインイン中にアカウントが認証済みかどうかを検証します。 lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります。 timeoutable 一定時間活動していないアカウントのセッションを破棄します。 omniauthable intridea/omniauthをサポートします。TwitterやFacebookなどの認証を追加したい場合はこれを使用します。 引用元:[Rails] deviseの使い方(rails6版)
https://qiita.com/cigalecigales/items/16ce0a9a7e79b9c3974eまた、Deviseは、コントローラーとビュー内で使用するヘルパーを作成します。
よく使用するコマンドを予め設定したものです。デバイスモデルが 'User'であると想定してヘルパー名は例示していますが、
デバイスモデルがユーザー以外の場合は、「_ user」を「_yourmodel(任意のモデル名)」に置き換えると、同じロジックが適用されます。こちらもQiita内で表にしてくださっている方がいらっしゃったので引用します。
メソッド 用途 before_action :authenticate_user! コントローラーに設定して、ログイン済ユーザーのみにアクセスを許可する user_signed_in? ユーザーがサインイン済かどうかを判定する current_user サインインしているユーザーを取得する user_session ユーザーのセッション情報にアクセスする 引用元:Rails deviseで使えるようになるヘルパーメソッド一覧
https://qiita.com/tobita0000/items/866de191635e6d74e392registrations -- サインアップ・アカウント編集・削除
最も基本的なアカウントのCRUDはこのregistrationが担っているので、ここが分からないと応用の機能部分のコードリーディングは難しそうです。
コード全体を上から調べた範囲で記入してきますが、ちまちま区切っていくので、分かりくいかもしれません。
横にソースコードを置きながら見てもらえれば、ちょっとは分かりやすいかも...frozen_string_literal
devise/app/controllers/devise/registrations_controller.rb# frozen_string_literal: trueコメントアウトされていますが、Rubyのバージョンアップに備えた1文のようです。
参考:frozen_string_literalが入って気づいた、メソッド設計の原則
https://qiita.com/jkr_2255/items/300b5db8c1f04e1e2815prepend_before_action
devise/app/controllers/devise/registrations_controller.rbclass Devise::RegistrationsController < DeviseController prepend_before_action :require_no_authentication, only: [:new, :create, :cancel] prepend_before_action :authenticate_scope!, only: [:edit, :update, :destroy] prepend_before_action :set_minimum_password_length, only: [:new, :edit]
DeviseControllerを継承しています。 ソースコードのファイルをみると、devise_controller.rbはこのモジュールだけでなく、すべてのモジュールへ引き継いでいます。prepend_before_actionはbefore_actionより前に実行するメソッドです。アクセスできるアクションがユーザーのログイン状態で制限されるようにしています。
- 参考:Railsドキュメント https://railsdoc.com/page/prepend_before_action
newアクション
devise/app/controllers/devise/registrations_controller.rb# GET /resource/sign_up def new build_resource yield resource if block_given? respond_with resource end
- ログインするための最初の部分です。
resourceはすでにdevise_controllerで定義されています。devise/app/controllers/devise_controller.rbdef resource instance_variable_get(:"@#{resource_name}") end # Proxy to devise map name def resource_name devise_mapping.name end alias :scope_name :resource_name
instance_variable_getメソッドはインスタンス変数の値を取得して返します。@user =からの定義と同じもののようです。
- 参考:Ruby 2.7.0 リファレンスマニュアル https://docs.ruby-lang.org/ja/latest/method/Object/i/instance_variable_get.html
resource内に#{resource_name}という変数がありますが、その下部分で定義されています。devise_mapping.nameこちらもすでにdevise_controllerで定義されてますが、引用記事をみると、別部分にヒントがありそうです。devise/app/controllers/devise_controller.rbdef devise_mapping @devise_mapping ||= request.env["devise.mapping"] endnameに注目してコードを繋げてみると、認証モデルがUserである場合は@singular = :users.to_s.tr('/', '').singularize.to_symとみれます。singularizeは複数形を単数形に変換するメソッドで、最終的に@singular = :userとなりsingularのエイリアスがnameとなっているのでmapping.nameで:userが取得できます。
するとdefinemethodsの引数に:userが渡されauthenticate_user!が出来上がるという流れになっています。
引用元:DeviseのコードリーティングでRailsを学ぶ
https://qiita.com/irisAsh/items/513b8b58f54421b9a1a0端的に言うと、
mapping.nameで:userが取得できるのでそれをresource_nameにしているということでしょうか。
registrations_controller下部にbuild_resourceがあります。セッションを新しく作るという意味のようです。devise/app/controllers/devise/registrations_controller.rbdef build_resource(hash = {}) #build_resource(hash = {})の定義 self.resource = resource_class.new_with_session(hash, session) end
- 元の
registrations_controller上部へ戻りましょう。buildはほぼnewと近い役割をしています。build_resourceという1文は、データベースから取り出したユーザーのインスタンス変数とセッションを作るということになります。block_given?はメソッドを実行する時にブロックが渡されていればtrueを返し、渡されていない時はfalseを返します。
- ブロックとは
do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。
引用元:Ruby 2.7.0 リファレンスマニュアル
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#block
この場合、resourceブロックを
yieldとして定義し、block_given?で真偽を確認している。yieldについて詳しく知る必要がありそうです。
- 参考:yield @variable if block_given? ってなに? https://kossy-web-engineer.hatenablog.com/entry/2020/01/19/094958-
- 参考:【Ruby入門】yieldの使い方まとめ https://www.sejuku.net/blog/20478
respond_with resourceはこの辺が関係して呼び出されるのかなと思いますが、devise/app/controllers/devise_controller.rbdef respond_with_navigational(*args, &block) respond_with(*args) do |format| format.any(*navigational_formats, &block) end enddevise/app/controllers/devise_controller.rbdef navigational_formats @navigational_formats ||= Devise.navigational_formats.select { |format|Mime::EXTENSION_LOOKUP[format.to_s] } endピンときていないので勉強して加筆・修正したいと思います。
ここまで複雑なんだなGem...たった数行のコードにいろいろなものが凝縮されていてとても勉強になりました。
newアクションしか書けなかったけど、最終的にはregistrations_controllerの各アクションのだけでも読んだ記録を残したい...!お付き合いいただきありがとうございました。































