ヨージとプログラミング

プログラミング勉強の記録

【Rails】ActiveRecord::StatementInvalid: Mysql2::Error: Error dropping database (can't rmdir './app_name_test', errno: 39): DROP DATABASE IF EXISTS `app_name_test`

RSpecを実行しようとしてエラー発生

RSpecをdocker環境で実行しようとしてタイトルのようなエラーが発生しました。

>>> docker-compose run web rspec spec/system/records_spec.rb  

rails aborted!
ActiveRecord::StatementInvalid: Mysql2::Error: Error dropping database (can't rmdir './app_name_test', errno: 39): DROP DATABASE IF EXISTS `app_name_test`

テスト環境のdbに何らかの異常があると思い、とりあえずリセットを試みました。
しかし、いずれのコマンドも同様のエラーになりました。

# すべてエラーに!
docker-compose run web rails db:reset RAILS_ENV=test
docker-compose run web rails db:drop RAILS_ENV=test
docker-compose run web rails db:migrate RAILS_ENV=test

mysql/app_name_testを手動削除して元気!

結局、ローカルに存在する、テストのデータベースを右クリック削除し、docker-compose run web rails db:create RAILS_ENV=test(からのmigrate)することで解決しました。
[:300] RSpecも無事に動くようになりました。

【Rails】RSpecでインスタンス変数を変更した後はreloadしよう

createした後にインスタンス変数を設定したい場合の話

RSpecでテストを行う前にletでインスタンスを生成することが多いと思います。

# 例 productクラスのインスタンスを生成しproduct_1に代入
let(:product_1) { create(:product, name: 'macbook') }

上の例では生成したインスタンスが値が'macbook'のname変数を持ちます。生成と同時に定義されています。
では、次のような場合どうなるでしょう?

# name変数なしで生成
let(:product_1) { create(:product) }
# 後でnameを定義
product_1.name = 'macbook'

1つ目の例と一緒のようですが、これではnameは未定義のままでした。(コンソールでたたけ無いので実行結果を載せれませんすみません)

reloadしないと適応されないぞ♪

# name変数なしで生成
let(:product_1) { create(:product) }
product1.name = 'macbook'

product_1.reload

reloadをしてあげることでしっかりと反映されrspecもすべてクリアできました。

【Rails】Could not find rake-12.3.1 in any of the sourcesのエラー解消

環境

Docker for mac gem 'spring-commands-rspec'

bin/rspecでrakeが存在しないと言われた

結構あるあるなエラーだと思います。
spring-commands-rspecというrspecを高速化するgemをインストールし、試しにdocker-compose exec web bin/rspecとしたらエラーが出ました。

➜ $docker-compose exec web bin/rspec
Could not find rake-12.3.1 in any of the sources
Run `bundle install` to install missing gems.

そしてGemfile.lockを確認すると当然のようにrakeの12.3.1がインストール済みであるんです。さて困ったとなるわけです。

なんで解決したかはまだ調査中

ruby on rails5 はまったこと · GitHub
こちらの記事を参考に

bundle install --binstubs

とすると

➜  $docker-compose exec web bin/rspec                
No examples found.

Finished in 0.00103 seconds (files took 0.48512 seconds to load)
0 examples, 0 failures

なおりました。
なぜなんだ…

【Rails5】Active::Recordが存在しないときにnilを返したい(find_byを使おう)

find(id)では存在しない時はエラーになる

[2] pry(main)> p = Spree::Product.find(0)
  => Spree::Product Load (0.9ms)  SELECT  `spree_products`.* FROM `spree_products` WHERE `spree_products`.`deleted_at` IS NULL AND `spree_products`.`id` = 0 LIMIT 1
ActiveRecord::RecordNotFound: Couldn't find Spree::Product with 'id'=0 [WHERE `spree_products`.`deleted_at` IS NULL]
from /potepanec/vendor/bundle/ruby/2.5.0/gems/activerecord-5.2.1/lib/active_record/relation/finder_methods.rb:346:in `raise_record_not_found_exception!'

(モデルがSpreeなのは無視してください)
この通り存在しないid=0を取得しようとするとNotFoundエラーとなりました。
しかし、エラーではなくnilが欲しいときがあると思います。
例えば、" || "を使って、いくつかの方法で取得を試みる場合。

@user = User.find_by(id: params[:id]) || User.find_by(name: params[:name])

上では最初にidでuserの取得を試みます。そこで取得できなかった場合はnilとなり、次にnameで取得することができます。
ポイントは、idで取得する際に、find(id)ではなくfind_by(id: ...)で取得していることです。find_byの場合、存在しないときはnilを返します。

【Rails5】SpreeおよびSolidusの仕組みメモ(執筆中)

公式ドキュメントより 「あるproductは最低1つのSpree::OptionTypeを持つ必要あり」
→しかしproductモデルにはSpree::OptionTypeの外部キーなし。中間テーブルSpree::ProductOptionTypeを通して保有していると思われる。 例)複数の色を提供したければ、"Color" option typeを作る必要あり。

「あるOption Typeは、最低1つのSpree::OptionValueと連携している必要がある。」
例)"Color" option typeが100以上のoption valueを持つこともありうる。
→ Spree::OptionValueがSpree::OptionTypeの外部キーをもつ。

Variantの使用

「master variantのみの場合、master variantがLineItemとつながる。」
「複数のvariantをもつ場合、master variantがLineItemに情報を与えることはない。(masterを買い物カゴにいれることが不可)」

variantのメソッド variatn.options_text: amount_stock_items(variant): variant.stock_items variant.option_value("tshirt-size") @variant.display_price

productメソッド product.master.id: product.display_price: product.images.first.attachment(:product): image.attachment(:product) image.attachment(:large)

【Rails5】find_byで取得するobjは1つ、whereは全部

スクール課題中に気づく

今までの誤解がやばかったので自分用のメモ
このQiitaのおかげで今までの誤解がとける

【rails】find・find_by・whereについてまとめてみた - Qiita

要約すると

# 条件に該当するもののうち最初の1つのみ取得
@images = Image.find_by(product_id: p.id)

# 条件に該当するものをすべて取得
@images = Image.where(prodcut_id: p.id)

てっきりfind_byでもすべて取得するかと思っていました。

【Rails5】コメント機能でのStrongParameterの設定方法

ハッシュが2つ出てくる

私が今回実装したコメント投稿formはこんな感じ。

= form_with(model: [@post, @comment]) do |f|
  .form-group
    = f.text_area :body, placeholder: 'コメント追加.', class:'form-control'
    = f.submit '投稿する', class:'btn'

一見普通ですがいつもと違うのはmodel: [@post, @comment]の部分です。普段であれば、渡すmodelは1つですが、今回は関連元、関連先の2つのインスタンスを指定します。
これによって、POSTしたときに渡されるパラメーターが以下のようになります。

Parameters: {
   "utf8"=>"XXX", 
   "authenticity_token"=>"XXXXXX", 
   "comment"=>{"body"=>"はじめまして!"}, 
   "commit"=>"XXXXX", 
   "post_id"=>"8"
}

commentテーブルに保存するデータは、"comment"=>{"body"=>"はじめまして!"}"はじめまして!"で、params.require(:comment).permit(:body)でストロングパラメーターを作ればいいじゃないかと思うかもしれません。しかし、commentsテーブルには:post_idのカラムもあるため、 "post_id"=>"8"の保存も必要です。

単純な発想ではArgumentErrorになる

まず思いつくのがこのような形かもしれません。これはエラーです。

params.require(:comment, :post_id).permit(:body)

では:post_idをpermitの方に入れてしまうのはどうでしょう? これももちろんエラーです。:post_idは:commentの値ではありませんので。

params.require(:comment).permit(:body, :post_id)

そもそもrequire, permitとは何か

こちらの記事 RailsのStrong Parametersを調べる - Qiita で紹介されていますが、requireは以下のように定義されています。

def require(key)
  self[key].presence || raise(ActionController::ParameterMissing.new(key))
end

self[key]の 部分は、アクセプター(params)の中に引数で渡したkeyのvalueが存在しているとtrueを返すようです。
他の記事ではrequire()の引数を2つ以上にしていてもいけるようなことが書かれていましたが、今回の場合はエラーとなりました(なぜだ)

引数を2つ指定
params.require(:comment, :post_id)

=>ArgumentError (wrong number of arguments (given 2, expected 1)):

次にpermit()ですが

def permit(*filters)
  params = self.class.new

  filters.flatten.each do |filter|
    case filter
    when Symbol, String
      permitted_scalar_filter(params, filter)
    when Hash then
      hash_filter(params, filter)
    end
  end

  unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters

  params.permit!
end

これははっきりと引数を複数とれることが明記されていますね。 処理も単純で渡されたパラメーターを許可するようです。

個別にStrongParameterして連結する

解決方法ですが、合わせてStrongParameterを指定することが困難でしたので、:commnetと:post_idで別個に指定し、後でmerge()でハッシュを連結しました。

class CommentsController < ApplicationController
  def create
    comment = current_user.comments.build(comment_params)
    comment.save
    〜
  end

  private
  def comment_params
    params.require(:comment).permit(:body).merge(params.permit(:post_id))
  end
end

これで一応うまくいきました。
きっともっとクールな方法があるんでしょうが思いつきませんでした。 知っている方ぜひ教えて下さい!