- 投稿日:2020-07-15T23:48:53+09:00
【Rails】createメソッドで保存ができない時に使える「pry-rails」
経緯
ポートフォリオ作成中にモデルの保存がなかなかできずにハマってしまった。
「pry-rails」というgemを使うことで解決できたのでメモします。コントローラ
以下のようなコード、メソッドを組んでました。
records_controller.rbclass User::RecordsController < User::Base ##中略... def create @record = current_app_user.records.build(record_params) if @record.save flash[:success] = '正常に入力されました' redirect_to user_root_url else flash.now[:danger] = '正常に入力されませんでした' render :new end end ##中略... private def record_params params.require(:record).permit( :material, :study_date, :study_hour, :study_minute, :memo, ) end endモデル
モデル名:record
カラムは以下の通りです。["id", :integer] ["material", :string] #教材を入れたい ["study_date", :date] #日付を入れたい ["study_hour", :integer] #勉強時間(hour)を入れたい ["study_minute", :integer] #勉強時間(minute)を入れたい ["memo", :string] #メモがきを入れたい ["app_user_id", :integer] ["created_at", :datetime] ["updated_at", :datetime]解決手順
Gemfileに以下追記
Gemfile.group :development, :test do gem 'pry-rails' endターミナルにてbundle install
$bundle installクリエイトメソッドに追記
records_controller.rbclass User::RecordsController < User::Base ##中略... def create binding.pry #こいつを追記すること!! @record = current_app_user.records.build(record_params) if @record.save ##中略...その上で、保存を試して見よう。そうするとGUIは待機状態になり
ターミナル上でコマンドライン操作が可能になる。
以下画面になる。From: /apps/study-meter/app/controllers/user/records_controller.rb:20 User::RecordsController#create: 19: def create => 20: binding.pry 21: @record = Record.new(record_params) 22: if @record.save 23: flash[:success] = '正常に入力されました' 24: redirect_to user_root 25: else 26: flash.now[:danger] = '正常に入力されませんでした' 27: render :new 28: end 29: end [1] pry(#<User::RecordsController>)>ここで、paramsと入力。すると
[1] pry(#<User::RecordsController>)> params => <ActionController::Parameters {"authenticity_token"=>"y5Y+hutJRPbyX9VM0lpiIax4hfeF5TXoykVL35fLQV727TmH/+/f/qncyRtDaANL1h5kqIOErcrGRhfCYuYCKg==", "record"=>{"study_date"=>"2020-07-14", "memo"=>"日本史", "study_hour"=>"0", "study_minute"=>"3"}, "commit"=>"投稿", "host"=>"study-meter.com", "controller"=>"user/records", "action"=>"create"} permitted: false>こうみてみると、どのカラムになんの値を入れようとしていたのかがわかる。
「memo」のカラムには、メモがきを入れようとしており、materialのカラムに、「日本史」を入れようとしていた。それなのにmemoのカラムに日本史が入っているたり、materialのカラムにはそもそも入力がされてない。
そこで、「おかしい」と気づく。materialはnot null制約をしていたから
「materialはnilじゃ駄目なのにnilのまま保存しようとするってどういうことやねん。」という流れで
保存がうまくいかなかったのでしょう。・メソッドは問題ないはず
・意図しないカラム意図しないカラムを入力しようとしてるこの点からviewのformの書き方がおかしいとあたりをつけることができた。
案の定そこのコードがおかしかったのでそこを直して解決した。結論
pry-railsは、エラー原因特定にすごく便利なライブラリでした。
- 投稿日:2020-07-15T23:13:46+09:00
Digdagで環境毎に設定値を変える(RubyOnRails)
やること
Ruby on Rails チュートリアルのサンプルアプリケーションにDigdag用YAML形式の環境設定ファイルを作ってRubyタスクから環境変数を取得しDigdag環境変数に設定、別のRubyタスクからDigdag環境変数を取得できるまで確認しようと思います。
今回の説明にはRuby on Rails チュートリアルのサンプルアプリケーショコードは関係ありませんが次回のバッチ実装で使うため、タウンロードしてそのフロジェクトにDigdagのWorkflowを追加します。
Ruby on Rails チュートリアルのサンプルアプリケーションダウンロード
https://github.com/yasslab/sample_app
workflow生成
$ digdag init workflow $ cd workflow $ digdag run workflow.dig以下のような結果が表示されればDigdagのWorkflow環境は問題なし〜
結果2020-07-15 22:01:56 +0900 [INFO] (0017@[0:default]+workflow+setup): echo>: start 2020-07-15T00:00:00+00:00 start 2020-07-15T00:00:00+00:00 2020-07-15 22:01:57 +0900 [INFO] (0017@[0:default]+workflow+disp_current_date): echo>: 2020-07-15 00:00:00 +00:00 2020-07-15 00:00:00 +00:00 2020-07-15 22:01:57 +0900 [INFO] (0017@[0:default]+workflow+repeat): for_each>: {order=[first, second, third], animal=[dog, cat]} 2020-07-15 22:01:57 +0900 [INFO] (0018@[0:default]+workflow+repeat^sub+for-0=order=0=first&1=animal=1=cat): echo>: first cat first cat 2020-07-15 22:01:57 +0900 [INFO] (0019@[0:default]+workflow+repeat^sub+for-0=order=1=second&1=animal=0=dog): echo>: second dog second dog 2020-07-15 22:01:58 +0900 [INFO] (0020@[0:default]+workflow+repeat^sub+for-0=order=1=second&1=animal=1=cat): echo>: second cat second cat 2020-07-15 22:01:58 +0900 [INFO] (0022@[0:default]+workflow+repeat^sub+for-0=order=2=third&1=animal=1=cat): echo>: third cat third cat 2020-07-15 22:01:58 +0900 [INFO] (0021@[0:default]+workflow+repeat^sub+for-0=order=2=third&1=animal=0=dog): echo>: third dog third dog 2020-07-15 22:01:58 +0900 [INFO] (0017@[0:default]+workflow+repeat^sub+for-0=order=0=first&1=animal=0=dog): echo>: first dog first dog 2020-07-15 22:01:58 +0900 [INFO] (0017@[0:default]+workflow+teardown): echo>: finish 2020-07-15T00:00:00+00:00 finish 2020-07-15T00:00:00+00:00YAML形式の環境変数ファイル作成
$ cd workflow $ mkdir config $ vi environment.ymldevelopment, staging, production別にurl, user,passwordを設定しました。
この環境変数をDigdagのタスクから参照します。environment.ymldevelopment: url: development.jp user: development_user password: development_password staging: url: staging.jp user: staging_user password: staging_password production: url: procuction.jp user: procuction_user password: procuction_passwordYAMLファイルの環境変数をDigdagの環境変数に設定するRubyクラスを作ります。
workflow
フォルダの下にtasks
フォルダを作ります
そこにenvironment.rbを作ります。こちらのクラスがYAMLファイルから環境変数を読み取ってDigdagの環境変数に保存します。$ cd workflow $ mkdir tasksenvironment.rbrequire 'yaml' class Environment def load environment = Digdag.env.params['environment'] environment = case environment when 'stg' 'staging' when 'prd' 'production' else 'development' end # 該当する環境の環境変数を取得 config = YAML.load_file('config/environment.yml')[environment] config.each do |key, value| # Digdag環境変数に保存 Digdag.env.store(key.to_sym => value) end end endWorkflow.digにタスク追加
$ digdag init workflow
で自動生成されたコードは削除して以下のコードを追加workflow.digtimezone: UTC +load_environment: rb>: Environment.load require: 'tasks/environment' +print_environment_value: sh>: echo url:${url} user:${user} password:${password} +print_ruby: rb>: DigdagEnvironment.show require: 'tasks/digdag_environment'■load_environmentタスク:Digdag実行の時の環境パラメーターから該当する環境変数をDigdag環境変数に設定するタスク
■print_environment_value:Digdag環境変数をシェルタスクから出力するタスク
■print_ruby:RubyクラスからDigdag環境変数を出力するタスクdevelopment環境から実行してみます。
$digdag run workflow.dig -p environment=development --rerundevelopment環境で実行結果2020-07-15 22:49:35 +0900 [INFO] (0017@[0:default]+workflow+load_environment): rb>: Environment.load 2020-07-15 22:49:35 +0900 [INFO] (0017@[0:default]+workflow+print_environment_value): sh>: echo url:development.jp user:development_user password:development_password url:development.jp user:development_user password:development_password 2020-07-15 22:49:36 +0900 [INFO] (0017@[0:default]+workflow+print_ruby): rb>: DigdagEnvironment.show url: development.jp user: development_user password: development_password次はStaging環境
$digdag run workflow.dig -p environment=stg --rerunstaging環境で実行結果2020-07-15 22:55:33 +0900 [INFO] (0017@[0:default]+workflow+load_environment): rb>: Environment.load 2020-07-15 22:55:33 +0900 [INFO] (0017@[0:default]+workflow+print_environment_value): sh>: echo url:staging.jp user:staging_user password:staging_password url:staging.jp user:staging_user password:staging_password 2020-07-15 22:55:34 +0900 [INFO] (0017@[0:default]+workflow+print_ruby): rb>: DigdagEnvironment.show url: staging.jp user: staging_user password: staging_password最後にProduction環境
$digdag run workflow.dig -p environment=prd --rerunproduction環境で実行結果2020-07-15 22:56:30 +0900 [INFO] (0017@[0:default]+workflow+load_environment): rb>: Environment.load 2020-07-15 22:56:30 +0900 [INFO] (0017@[0:default]+workflow+print_environment_value): sh>: echo url:procuction.jp user:procuction_user password:procuction_password url:procuction.jp user:procuction_user password:procuction_password 2020-07-15 22:56:31 +0900 [INFO] (0017@[0:default]+workflow+print_ruby): rb>: DigdagEnvironment.showこれで環境変数設定が終わったので次回からはRubyOnRailsのチュートリアルアプリのデータでバッチを作ってみようと思います。
- 投稿日:2020-07-15T22:16:27+09:00
freeeAPI 請求書発行したら、自動で+更新までやっちゃおうぜ!!
こんにちは、未経験からエンジニア転職を目指しているmkです。
今回はfreeeAPIを触ってみました。表題の通り、請求書をアップロードして+更新を毎回ぽちぽちやるのが面倒なので、
請求書をAPI経由で発行して、+更新まで全てやってくれるプログラムをRubyで書いてみました。環境
macOS 10.15.5
ruby 2.7.1そもそも+更新とは
前受けが発生する場合のfreeeの活用法(外部リンク)
こちらの記事にわかりやすくまとまっていますが、
要するに、前受金を売上に変更する作業ができるボタンです。(間違ってたらすいません、、、)freee謹製前受管理アプリがfreeeアプリストアにはあるのですが、
請求書の発行からnヶ月分前受した売上金を月次の売上に分割する作業を一から自動化したかったので今回作成しました。必要な処理
- アクセストークンの発行(GET)
- company_idの取得(GET)
- 勘定科目idの取得(GET)
- 取引先idの取得(GET)
- 税区分idの取得(GET)
- 請求書の発行(POST)
- 取引詳細の取得(GET) *取引行idの取得
- +更新の作成(POST)
請求書発行→+更新のコード
まずはhelper的な処理を記述していきます。
helper.rb#月の処理 def add_month(num) if num < 12 return num = num + 1 elsif num == 12 num = 1 end end # 初月のプラス更新額を求める処理 def first_price(price,times) (price % times) + (price / times) end請求書発行月を含む更新回数月分の月末を取得しないといけないので、add_monthを定義。
また、請求金額が更新回数で割り切れない場合、初回の更新に余を含めるようfirst_priceを定義します。
httpリクエストのヘッダーを指定するモジュール
set_header.rbmodule Header def self.get_header headers = { "accept" => "application/json", "Authorization"=> "Bearer #{YOUR_ACCESS_TOKEN}" } end def self.post_header headers = { "accept" => "application/json", "Authorization"=> "Bearer #{YOUR_ACCESS_TOKEN}", "Content-Type" => "application/json" } end end請求書発行のコード
invoice.rbrequire 'set_header.rb' require 'json' require 'net/http' class Invoice BASE_URL = 'https://api.freee.co.jp' #パラメータを引数に渡すことで請求書を作成 def self.make_invoice(params) uri = URI.parse(BASE_URL + '/api/1/invoices') http = Net::HTTP.new(uri.host,uri.port) http.use_ssl = uri.scheme === "https" req = Net::HTTP::Post.new(uri.path) req.body = params.to_json req.initialize_http_header(Header.post_header) response = http.request(req) res_hash = JSON.parse(response.body) end endプラス更新作成のコード
koushin.rbrequire_relative 'set_header.rb' require 'json' require 'net/http' class Koushin BASE_URL = 'https://api.freee.co.jp' #取引id,パラメータを引数に渡すことでプラス更新をかける def self.post_koushin(torihiki_id,params) uri = URI.parse(BASE_URL + "/api/1/deals/#{torihiki_id}/renews") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme === "https" req = Net::HTTP::Post.new(uri.path) req.body = params.to_json req.initialize_http_header(Header.post_header) response = http.request(req) res_hash = JSON.parse(response.body) end endそして請求書発行からプラス更新までを実装
請求金額は12345678円(外税)、税率は10%に
+更新回数は12回で処理を実行してみます。invoice_koushin.rbrequire 'invoice.rb' require 'koushin.rb' require 'date' require 'helper.rb' # その他いろいろrequireしてますが省略 #必要項目の設定 month = Time.now.month year = Time.now.year count = 1 company_id = Company.company_id #github参照 事業所idの取得 #請求額(外税) amount = 12345678 #消費税額(10%) vat = (amount * 0.1).floor #+更新回数 num = 12 # 請求書のパラメーター invoice_params = { "company_id": company_id, "issue_date": Date.today, "due_date": Date.new(year, month, -1), "partner_id": Supplier.supplier_id('CFO'), #github参照 取引先idの取得 "booking_date": Date.today, "description": "#{Date.today.month}月分請求書", "invoice_status": "issue", "partner_display_name": "CFO株式会社", "partner_title": "御中", "partner_contact_info": "営業担当", "partner_zipcode": "012-0009", "partner_prefecture_code": 4, "partner_address1": "湯沢市", "partner_address2": "Aビル", "company_name": "freee株式会社", "company_zipcode": "000-0000", "company_prefecture_code": 12, "company_address1": "XX区YY1−1−1", "company_address2": "ビル 1F", "company_contact_info": "法人営業担当", "payment_type": "transfer", "payment_bank_info": "XX銀行YY支店 1111111", "message": "下記の通りご請求申し上げます。", "notes": "毎度ありがとうございます", "invoice_layout": "default_classic", "tax_entry_method": "exclusive", "invoice_contents": [ { "order": 0, "type": "normal", "qty": 1, "unit": "個", "unit_price": amount, "vat": vat, "description": "備考", "tax_code": 2, "account_item_id": AccountItem.account_item_id('前受収益') #github参照 前受収益のaccount_item_idを取得 } ] } #請求書の発行 invoice = Invoice.make_invoice(invoice_params) puts '請求書を発行しました' #請求書発行のレスポンスから取引idを取得 torihiki_id = invoice['invoice']['deal_id'] #上記取引idの取引行idを取得 renew_target_id = Torihiki.target_id(torihiki_id) #github参照 torihiki_idに紐づく取引の詳細をGET、そこから取引行idを取得 #+更新時の勘定科目id(売上高)を取得 uriagedaka_id = AccountItem.account_item_id('売上高') #github参照 #取引id 取引行idを使ってnum回+更新をかける num.times do #countが1の場合+更新額をfirst_priceに変更 if count == 1 koushin_amount = first_price(amount,num) koushin_vat = first_price(vat,num) else koushin_amount = amount / num koushin_vat = vat / num end date = Date.new(year, month, -1) #+更新のパラメータをセット koushin_params = { "company_id": company_id, "update_date": date, "renew_target_id": renew_target_id, "details": [ { "account_item_id": uriagedaka_id, "tax_code": 21, "amount": (koushin_amount + koushin_vat), "vat": koushin_vat } ] } #取引id,パラメータを使って+更新の作成 Koushin.post_koushin(torihiki_id,koushin_params) puts "#{date}の更新をしました" # 翌月の月を取得 month = add_month(month) #monthが1月の場合西暦を1足す year += 1 if month == 1 count += 1 endローカルで実行してみます。
ちゃんと処理は走ったみたいです。実行時間は5秒程(ちょっと長い、、、)
会計freeeをみてみます。CFO株式会社宛に請求書が発行できてますね!
金額も消費税額もきちんと入ってます。
次は取引を見てみましょう。更新日付も発行月の月末含む、12ヶ月分の末日が入っています。
今後の課題
月末発行の請求書はどうするんだとか、振り込み確認できた翌月から更新をかけたいだとか、柔軟に対応できるよう改良していきたいと思います。
また請求書の各パラメータと更新回数をcsvでインポートできるようします。
あとコードが美しくない、、、笑日々精進
- 投稿日:2020-07-15T22:15:17+09:00
【memberとcollection】
memberとcollectionとは
現在ツイッターの写真投稿版のようなアプリを作成しているのですが、その作成途中で投稿の検索機能を実装する際に出てきたmemberとcollectionについてとそれぞれの違いについてアウトプットします。
memberとcollectionはルーティングを設定する際に使用しすることができ、生成されるURLと実行されるコントローラを任意にカスタムすることができます。
collectionはルーティングに:idがつかない、memberはつくという違いがあります。
上の画像のようにtweetsコントローラーのネストに入れてcollectionを記述すると、
以下の画像のルーティングになります。URIを見てもidが入っていません。
一方、memberで記述した場合
この場合のルーティングは以下の画像のようになります。
このようにルーティングにidが入っています。
今回作成した写真投稿アプリの場合は検索結果としてあくまで一覧表示させたいだけでidごとの詳細ページは必要ないのでcollectionを使用しました。
- 投稿日:2020-07-15T22:07:03+09:00
RaspberryPi3 + GPS受信キット +
Goal
- GPSで位置情報が取れること
使ったもの
- GPS受信機キット 1PPS出力付き 「みちびき」3機受信対応
- ラズパイ3B
- あとは抵抗とスイッチングダイオードくらい。
接続
手元に部品がそろっていたので秋月さまサイトに掲載されているマニュアルの回路図のまま組みました。スイッチングダイオードは実際は不要みたいですが、壊さないよう保険にもなるので繋ぎました。
http://akizukidenshi.com/download/ds/akizuki/AE-GPS_manual_r1.06_s.pdf
初期設定でGPSのアンテナが真上を向く必要があったので、ブレッドボードが縦になっています。
ちなみに初期動作テストでLEDが点滅するまで30分くらいかかりました。窓際に置いたのですが置いた場所が悪かったのかも。ソース
下記ソースを実行するにはラズパイでUART接続関連の設定をする必要があります。そこはググってください
それさえすめばあとは特に大したことはやっていません。なお、NMEAフォーマット解析用にnmea_plusを使いました。
SourceDecoder
まぢ便利require 'serialport' require 'nmea_plus' sp = SerialPort.new('/dev/serial0', 9600, 8, 1, 0) # see: https://rubydoc.info/gems/serialport/SerialPort#set_modem_params-instance_method source_decorder = NMEAPlus::SourceDecoder.new(sp) source_decorder.each_complete_message do |message| # see: https://github.com/ianfixes/nmea_plus/blob/master/lib/nmea_plus/message/nmea/rmc.rb if 'GPRMC' == message.data_type puts message.utc_time puts message.active? # false: データ無効 puts message.latitude puts message.longitude #puts message.speed_over_ground_knots #puts message.track_made_good_degrees_true #puts message.magnetic_variation_degrees puts message.faa_mode # A: 単独測位(精度3m程度), D: 相対測位(精度0.4m程度) puts end end sp.close結果
以下実行結果です。
2020-07-15 12:35:37 +0000 true 33.725575 131.64382333333333 A 2020-07-15 12:35:38 +0000 true 33.72558166666666 131.64381833333334 AちなみにGoogleだと、検索キーワードに緯度経度を入れるとその地点でGooleMapが表示されますよー。
姫島村のオフィスです感想
- GPS受信キットのピンの並びがラズパイのピンに合わせてある(と思われる)ので繋ぐとき迷わなかった。
- 探せばたいてい便利なGemが見つかりますねー。意地でもRubyを使おう。
- あ、1PPS端子繋いだのに遊ぶの忘れてた。
- 投稿日:2020-07-15T22:07:03+09:00
RaspberryPi3でGPS受信キットを使ってみる(Ruby)
Goal
- GPSで位置情報が取れること
使ったもの
- GPS受信機キット 1PPS出力付き 「みちびき」3機受信対応
- ラズパイ3B
- あとは抵抗とスイッチングダイオードくらい。
接続
手元に部品がそろっていたので秋月さまサイトに掲載されているマニュアルの回路図のまま組みました。スイッチングダイオードは実際は不要みたいですが、壊さないよう保険にもなるので繋ぎました。
http://akizukidenshi.com/download/ds/akizuki/AE-GPS_manual_r1.06_s.pdf
初期設定でGPSのアンテナが真上を向く必要があったので、ブレッドボードが縦になっています。
ちなみに初期動作テストでLEDが点滅するまで30分くらいかかりました。窓際に置いたのですが置いた場所が悪かったのかも。ソース
下記ソースを実行するにはラズパイでUART接続関連の設定をする必要があります。そこはググってください
それさえすめばあとは特に大したことはやっていません。なお、NMEAフォーマット解析用にnmea_plusを使いました。
SourceDecoder
まぢ便利require 'serialport' require 'nmea_plus' sp = SerialPort.new('/dev/serial0', 9600, 8, 1, 0) # see: https://rubydoc.info/gems/serialport/SerialPort#set_modem_params-instance_method source_decorder = NMEAPlus::SourceDecoder.new(sp) source_decorder.each_complete_message do |message| # see: https://github.com/ianfixes/nmea_plus/blob/master/lib/nmea_plus/message/nmea/rmc.rb if 'GPRMC' == message.data_type puts message.utc_time puts message.active? # false: データ無効 puts message.latitude puts message.longitude #puts message.speed_over_ground_knots #puts message.track_made_good_degrees_true #puts message.magnetic_variation_degrees puts message.faa_mode # A: 単独測位(精度3m程度), D: 相対測位(精度0.4m程度) puts end end sp.close結果
以下実行結果です。
2020-07-15 12:35:37 +0000 true 33.725575 131.64382333333333 A 2020-07-15 12:35:38 +0000 true 33.72558166666666 131.64381833333334 AちなみにGoogleだと、検索キーワードに緯度経度を入れるとその地点でGooleMapが表示されますよー。
姫島村のオフィスです感想
- GPS受信キットのピンの並びがラズパイのピンに合わせてある(と思われる)ので繋ぐとき迷わなかった。
- 探せばたいてい便利なGemが見つかりますねー。意地でもRubyを使おう。
- あ、1PPS端子繋いだのに遊ぶの忘れてた。
- 投稿日:2020-07-15T22:02:49+09:00
新卒1年目がTDDを活かした実装方法を考えさせていただきました(ruby)
なぜ新卒1年目がTDDに注目したのか
現在新卒1年目でエンジニアとして働いている。エンジニアリングの経験が浅い自分が、経験の浅さを活かして先輩に何かで勝てないか考えていた。
経験が浅いので実装手順に癖がない。癖がないからこそ、いい癖をつけておこうと考えてTDDを活かした実装に挑戦しようと思った(もっと違うやり方があったかもしれません)。
実際にやったこと
Ruby on Rails チュートリアルでTDD開発をしてみる。
sample_appテスト駆動開発をざっと読んでみる
テスト駆動開発の著者の動画を見てみる。
Clean code that works - How can we go there? - Takuto Wada | SeleniumConf Tokyo社内のTDDの勉強会の資料を閲覧
TDDとは
テスト駆動型開発。実装する前に、実装がうまく行った時にのみパスするテストを書くということ。
TDDのイメージ
テストを書く(実装前)
↓
テストが失敗する(何も実装していないから)
↓
実装をする
↓
テストが成功する(実装によって、実装前に書いたテストをパスできるようになったから)
↓
リファクタリングを行う。
これによりテストが成功していれば、機能を失わずにコードを修正できたと言える。
逆にリファクタリングにより、テストが失敗すれば、実装したはずの機能を失ってしまっているので、デバッグをする。TDDのメリット、デメリット
TDDのメリット
- 安全なリファクタ
- 機能追加によるバグが防げる
TDDのデメリット
- いちいち書くのはめんどくさい
- どんなテストを書けば良いかわからない
TDDするかの指標
TDDのデメリットを抑えて、メリットを活かすためにはどんな時にTDDするのかの指標が必要。
- 単純なテスト(例:リクエストが成功しているか)→TDD
- 動作、開発内容が完璧に決まり切っていない段階→開発が先
- セキュリティー的に重要な開発やバリデーションを確認したい時→TDD
- リファクタリングをしたい→先にテストを書いて、リファクタリングしてもテストがパスするか確認
要するに基本TDDしようという姿勢で良いが、まずテストどう書いたら良いかわからないと感じた時はTDDするべきではない。
TDDする時のテストの具体例
ここからTDDをしたテストの具体例をあげる。様々なテストの種類(model,controller,integrationなど)を用いた説明になるので、テストの種類について知らない方はこちらを見ていただければと思います。
railsのテストディレクトリ構造とテスト処理単純なテスト
リクエストした時に成功するか、必要な要素の存在確認(単純なcontroller test)。シンプルな確認テストは先に書くと良いということ
controller_test.rb#コントローラーテスト test "should get home" do get root_path assert_response :success is #リクエストが成功するか assert_select "title", "HOME TITLE" #ページのタイトルがHOME TITLEになっているか endバリデーション
バリデーションがうまく行っていれば成功するテストを、最初に書く。(例: model test, form送信の結果を確認するintegration test)
- モデルクラスに対してバリデーションの確認をする時
user_test.rb#ユーザーモデルのテスト #ユーザーモデルでバリデーション内容の記述をする前に、以下のテストを書く def setup @user = User.new(name: "Example User", email: "user@example.com", password: "hogehoge", password_confirmation: "hogehoge") end test "should be valid" do assert @user.valid? end test "name should be present" do @user.name = " " assert_not @user.valid? end test "email should be present" do @user.email = " " assert_not @user.valid? end
- formを使った送信(新規登録、ログインなど)に対してのバリデーションを確認する時
users_signup_test.rb#新規登録のテスト(integration_test)を行う #無効になるパラメータで登録しようとして失敗して欲しいテスト test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end end機能修正(デバッグ)のためのTDD
特にエラー文が出ているわけではないが、自分の思った通りに動いていない時に先にテストを記述。
自分の思った通りに動いた時にのみパスするテストを先に書き、その後自分の思った通りに動くようにコードに変更を加える。ログアウトの処理を例に挙げて説明する。Ruby on Rails チュートリアル 9.14(二つの目立たないバグ)を参考にいたしました。
Login_controller.rbclass LoginController < ApplicationController . . def destroy #ログアウトするためのアクション log_out redirect_to root_url end private #これ以降はアクション内で使用する関数の定義 # 永続的セッションを破棄する def forget(user) user.forget #DBに保存されているuserのログイン記憶トークンを空にする cookies.delete(:user_id) #cookiesの中身を空にする cookies.delete(:remember_token) end # 現在のユーザーをログアウトする #現在のユーザを示すcurrent_userの記憶トークンを空にし、session,変数current_userの中身も空にする def log_out forget(current_user) session.delete(:user_id) current_user = nil end endこのコードの状態で、
ブラウザで2つのタブを開き、それぞれのタブで
同じユーザーでログインしている状況を想定する。まず一つのタブでログアウトする。するとcurrent_userの値ががnilになる。
その後にもう一方のタブでログアウトしようとするとcurrent_userがnilなのでnil.forgetになり、エラーになってしまう。このような状況は、「特にエラー文が出ているわけではないが、自分の思った通りに動いていない時」と言えるので、TDDをする。ログアウト二回行った時に正常な動作になっているか確認するテストを先に書くのである。
users_login_test.rbdef setup #loginするユーザーインスタンスを定義 @user = User.new(name: "Example User", email: "user@example.com", password: "hogehoge", password_confirmation: "hogehoge") end test "two times logout after login" do log_in(@user) delete logout_path assert_not is_logged_in? assert_redirected_to root_url #別のタブでログアウトしたことをシュミレート delete logout_path follow_redirect! #リダイレクトすると、ログイン前のページになっているか確認 assert_select "a[href=?]", login_path #loginページのためのパスがあるか確認 assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 endこのテストをパスするために、機能に変更を加える。最終的に以下のようなコードを追加するとテストがパスする。
Login_controller.rbclass LoginController < ApplicationController . . def destroy #ログアウトするためのアクション log_out if logged_in? #login状態のときだけlogout関数使用 redirect_to root_url end endコツ・その他
テスト駆動開発という本からTDDを実装する時に使用するコツを学んだ。
この本には最低限テスト作成→仮実装→三角測量→リファクタリングをしようと書いてあった。
今回は入力された引数(int)を文字列にして返す関数を、作成するためのTDDを例にあげる。
最低限テスト作成
関数(まだ作成されていない)のテストを作成する。作成した時点ではテストがredになる。
test "Pattern1 of test returnString function " do a = returnString(1) assert_equal a, "1" end仮実装
どんな形であれ、上で書いたテストが通るような関数を定義する。関数定義でテストがgreenになる。
def returnString(int) return "1" end三角測量
同種のテストを複数作成(今回は2個)し、そレラのテストがパスするように実装を一般的な形に書き換える
- まず似たようなテストをもう一つ追加
このテストを作成するとredになってしまう。なぜなら今の実装では"1"しか返さないから。
test "Pattern2 of test returnString function " do b = returnString(2) assert_equal b, "2" end
- 実装を一般的な形に変更する
1という生の値を返す関数から、引数で受け取った値を文字列にして返す関数に変更する。テストはgreenになる。
def returnString(int) return "#{int}" endリファクタリング
上記のように最低限のな実装をするためにTDDをしても、さらに追加したい機能が増えて、コードは複雑になっていく。
でも大丈夫。テストを書いているおかげでコードの機能が正確は判断してもらえる。よって安心してリファクタリングができる。TDDを実践してみて感じたこと
TDDをすることで自分がまず何を実装したいのか考えれる。それが実装を効率的にしていると感じた。またテストを書いておくことで安心してリファクタリングができるのでコードの保守性を維持しやすくなるだろうなと感じた。
TDDに限らず、テストを記述する時は複種のテスト内容を一気に記述しようとしなくて良い。一気に書こうとしてtest内容自体が間違ってしまっては本末転倒
上の内容とかぶるがテストの内容を網羅的に書きすぎることはよくない。網羅的に書きすぎる(HTML要素の存在を徹底的に確認等)と、今後機能に変更があった場合テスト自体も保守、修正する手間が増えるから。
TDDに限らず、バリデーションのテストを記述する時は、うまく行って欲しい時のテストとうまく行ってほしくない時のテストの両方書かなければいけない。
users_signup_test.rb#新規登録のテストを行う #無効になるパラメータで登録しようとして失敗して欲しいテスト test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end end #有効になるパラメータで登録しようとして成功して欲しいテスト test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end end
- raiseを記述することで、その場面で意図的にエラーを発生させる。raiseを記述したのにも関わらず、テストが全て通るということは、raiseを記述した部分がテストされていないことになる。条件分岐(if文)の中でraiseを記述して、その分岐がテストされているか確認する。テストされているか怪しい部分にraiseを書いて、テスト実施されているか確認するとよい。
controllerdef create if (user_id = session[:user_id]) . . else raise # テストがパスすれば、この部分がテストされていないことがわかる→この部分がテストされるようにテストを書く . . end end参考文献
Ruby on Rails チュートリアル
テスト駆動開発
Clean code that works - How can we go there? - Takuto Wada | SeleniumConf Tokyo
- 投稿日:2020-07-15T21:46:12+09:00
docker-composeによるRails6(APIモード)+MySQLのDocker環境作成(for Mac)
はじめに
バイト先で全面改修を進めていく上で、環境をdocker化しようという話が出ました。
フロントエンドNuxt.js、バックエンドRails6の構成で作ることになり、バックエンド部分を自分が担当することになったのでDockerfileをごりごり書いていきましょー!ということで勉強も兼ねて実装し、出来上がった構成を紹介します。
Railsコンテナの作り方は、Dockerの公式ページに簡単に書いてありますが、今回はRails6に考慮したDockerfileの書き方と、MySQLコンテナの作り方も合わせて紹介していこうと思います。自身のスキル
個人でrailsを触り始めたのは1年半ほど前、webエンジニアとしての実務での開発経験は1年強と期間的には長くはありません。
しかし、大学でつけた幅広い前提知識や、どんどん色々なことに挑戦する姿勢を利用して素早く成長しています。
前のプロジェクトでもdocker周りには触らせていただいていて、データベースのコンテナの変更(MarinaDB->MySQL)や、初期設定用のシェルスクリプトの作成などを行っていました。
自分で1からdockerを作る機会がなかったので今回挑戦した形です。docker環境作成
ここから先は実際にdockerによる環境を作っていきます。
ファイル構成
まずは簡単にファイル構成を紹介します。
作成するアプリケーションの名前をsample
とすると、今回関係するファイルは以下のようなファイル構成になっています。sample |- backend --- app | |- bin | |- config --- ... | | |- database.yml | | |- ... | |- ... | |- Dockerfile | |- Gemfile | |- Gemfile.lock | |- ... |- db --- data --- ... | |- my.cnf |- docker-compose.yml実際はもっと多くのファイルが存在する(作成される)ことになりますが、今回操作するファイルはこれらのファイルのみになります。
必要ファイルの作成
続いてdockerに関するファイルを記述していきます。
backend/Dockerfile
これはRailsのコンテナをビルドするための手順を記したファイルになります。内容を下に載せ、それぞれどのような処理を行っているのかをコメントで説明しています。
backend/Dockerfile# 使用するイメージとバージョン FROM ruby:2.7.1 # 必要なライブラリをインストール RUN apt-get update -qq && \ apt-get install -y build-essential \ libpq-dev \ nodejs # 以下はrails6以降(APIモード除く)で必要 RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn # コンテナ内にappというフォルダを作成 # ※ローカルPCにフォルダが作成されるわけではない RUN mkdir /app # ルートをappディレクトリに変更 ENV APP_ROOT /app WORKDIR $APP_ROOT # ローカルPC内のGemfile(.lock)をコンテナ内にコピー ADD ./Gemfile $APP_ROOT/Gemfile ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock # bundle installを実行しローカルPCのファイルたちをコンテナ内にコピー RUN bundle install ADD . $APP_ROOT # コンテナがlistenするポート番号 EXPOSE 3000 # 実行されるコマンド CMD ["rails", "server", "-b", "0.0.0.0"]rails6からはWebpackerがデフォルトとなったので、
yarn
がインストールされていないとrails new
をするときにエラーとなってしまいます。
ただし、後述のAPIモードで作成する場合にはyarn
のインストールは不要です。backend/Gemfile
Railsをインストーするためにあらかじめ
Gemfile
を記述しておきます。
このファイルは後ほどrails new
をするときに書き換えられます。
ここではrails6以降のバージョンを指定しています。backend/Gemfilesource 'https://rubygems.org' gem 'rails', '~> 6'backend/Gemfile.lock
bundle install
した際に依存関係を元にライブラリの設計図のようなものを作ってくれます。
空のものを作成しておきます。ファイルが存在しないと後でエラーになりました。backend/Gemfile.lockdb/data/
ディレクトリだけ用意しておいてあげましょう
db/my.cnf
MySQLの設定に関するファイルです。
my.cnf[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci sql_mode='' [client] default-character-set=utf8mb4文字コードの設定や
sql_mode
の設定を行っています。
特にsql_mode
の設定はしておいた方が良いと思います。
というのも、MySQL5.7からデフォルトのsql_mode
が変更となっており、特にonly_full_group_by
などのモードで度々エラーを吐いてしまうことがあるので、どのsql_mode
にするかは明示的に書いておきましょう。今回は設定なし(MySQL5.6.5以前のデフォルト)を明示的に指定しています。
(この辺りでも追々記事が書きたい)docker-compose.yml
いよいよdocker-composeを書きます。
簡単な説明をコメントで記述してあります。docker-compose.ymlversion: "3" services: # MySQL db: #ビルドするイメージ image: mysql:5.7 # 環境変数の指定 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: spportunity MYSQL_USER: root MYSQL_PASSWORD: root TZ: 'Asia/Tokyo' # 文字コードをutf8mb4に設定 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: # db/dataディレクトリをコンテナのmysqlディレクトリにマウント - ./db/data/:/var/lib/mysql # /db/my.cnfファイルをコンテナ内のmy.cnf設定ファイルにマウント - ./db/my.cnf:/etc/mysql/conf.d/my.cnf ports: - 3306:3306 #Rails backend: # ビルド元の指定 build: context: ./backend dockerfile: Dockerfile # コンテナ名の指定 container_name: "sample-backend" # 起動時のコマンド指定 前のプロセスを終了してからrails sをしている command: bash -c "rm -f tmp/pids/server.pid && rails s -p 3000 -b '0.0.0.0'" # backendフォルダ内のファイルをコンテナ内のappディレクトリにマウント volumes: - ./backend/:/app ports: - "3000:3000" # dbコンテナよりも後に起動 depends_on: - dbこれでファイルの準備はできたのでrailsアプリケーションを作成していきます。
railsアプリケーションの作成
docker-compose
コマンドを利用してrailsアプリケーションを作成します。
現在のディレクトリがsample
であることを確認してください。通常モードでrailsを作成(dockerfileでyarnのinstallが必要)
% docker-compose run backend rails new . --force --no-deps --database=mysql --skip-bundle
--force
...Gemfile
を書き換えます--database=mysql
...データベースにMySQLを指定します--skip-bundle
...bundle
をスキップします(まだGemfile.lock
は変更されません)APIモードでrailsを作成
% docker-compose run backend rails new . --force --no-deps --database=mysql --skip-bundle --api
--api
...APIモードでの作成オプション完了すると、
backend/
フォルダ内にrails関連のフォルダやファイルが大量に出来上がります。
APIモードと通常モードの違いやファイルの差分はまた記事にします。コンテナのビルド
% docker-compose buildこのコマンドを実行することで、MySQLのコンテナの作成と、railsコンテナでの
bundle install
が行われます。
コンソールを見ているとDockerfileの内容を順番に実行している様子が分かるかと思います。railsのdbへの接続設定
backend/config/database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: root host: db development: <<: *default database: sample test: <<: *default database: sample_test production: <<: *default database: sample_production username: <%= ENV['APP_DATABASE_USER'] %> password: <%= ENV['APP_DATABASE_PASSWORD'] %>接続情報を、hostを先ほど作成したMySQLコンテナのイメージ、ユーザー名とパスワードを先ほど作成したコンテナの環境変数のものに変更します。
(APIモードでなければ)Webpackerのインストール
% docker-compose run backend rails webpacker:installデータベースの作成
% docker-compose run backend rails db:createdockerの起動
ここまで来れば設定は完了です。
あとはdockerを起動するのみです!% docker-compose upブラウザでRailsが立ち上がっていること確認
ブラウザで http://localhost:3000/ へアクセスすると、無事Railsが立ち上がっています。
拡張性
今回は言及しませんでしたが、frontendコンテナなどを追加することによってフロントエンドアプリケーションとAPIサーバを兼ね備えた環境を一発で作ることができるようになります。
その際は、同様の手順でDockerfileの作成やdocker-composeへのコンテナの追加などを行っていくことになりまね。
僕はまだフロントエンド初心者なので、ゆくゆくはフロントのコンテナも作っていきたいです。感想
やっぱりdockerは便利ですね。
自分個人の開発ではdockerは使っていませんでしたが、これからは積極的に使っていこうかなと思っています。
またその際には記事にしてあげていければと思います。qiitaの記事を書くのが初めてで、どれくらいのものを書けばいいのか悩みながら書いていたら、だいぶ長くなってしまいました。
ただ、とても内容が濃い記事を何本も書いている人がたくさんいて、自分はまだまだだなぁと日々感じています。
僕はまだ内容があるものは頻繁には書けないかもしれませんが、少しずづ知識を記事化していければと思います。参考にしたもの
以下のqiitaの記事を参考にさせていただきました。
書き方も真似させていただき、かなり参考になりました。
ありがとうございます。https://qiita.com/azul915/items/5b7063cbc80192343fc0
https://qiita.com/kodai_0122/items/795438d738386c2c1966上記の記事に加えて、APIモードやMySQLコンテナのマウントや設定なども盛り込んだ内容となっているので、書き方が似ている点はご容赦いただければと思います。
- 投稿日:2020-07-15T21:04:52+09:00
【Rails 退会】railsで簡易的な退会機能を作る
【Rails 退会】railsで退会機能を作る
前提条件
モデル:Userモデル
コントローラー:Usersコントローラー
ルーティング:resources :usersを記述機能をつける前の実装プロセス設計
①投稿機能をつける場合と同様に、Userのidをキーとしてレコードを取得
②そのレコードをdestroyメソッドを用いて、削除する実際にやってきましょう!
Users_controller.rbをいじいじ
users_controllerclass UsersController < ApplicationController def destroy @user = User.find(params[:id]) #特定のidを持つ情報を取得 @user.destroy flash[:success] = 'ユーザーを削除しました。' redirect_to :root #削除に成功すればrootページに戻る end endviewをいじいじ
〇〇.html.erb<%= link_to "退会する",user_path(current_user.id), class: "destroy-user",method: :delete %> <%# ここで、(current_user.id)でしっかり引数にログインユーザーのidを渡してあげること!これだけで完了!
高度な退会機能はまだ作れなかった...
とりあえず!!一旦これで!
- 投稿日:2020-07-15T20:49:36+09:00
初心者がはまりがちなDockerのエラーに対する解決法
記事の背景
Le Wagon Tokyo (https://www.lewagon.com/ja/tokyo) という、主にRubyとRailsを教えるフランス発の英語のプログラミングスクールを卒業後、Dockerを勉強し始めました。卒業後に取り組んだプロジェクトでDockerの構築を行ったのですが、多くのエラーに直面しました。独学で試行錯誤しながらエラー解決を行ったので、非常に多くの時間がかかってしまいました。私のように、Dockerを初めて触ってエラーに苦労した人も多いと思うので、私が使った解決方法を参考にしていただければと思い、この記事を書くことにしました。
1 PermissionError (Permission denied :‘~~~/your_app/tmp/db’)
Dockerの公式ドキュメントを参照する限り、このエラーはLinuxユーザーだけのようですが、LinuxユーザーにとってPermissionは厄介になりうるので共有します。このエラーは、Dockerが一時的なデータベース(dbフォルダ)を作るのですが、現在ログインしているLinuxユーザーがそのデータベースを使う権利がないため発生します。
まずはこのコマンドでそのテンポラリーフォルダに移動します。
$ cd ~~~(ご自身のアプリまでのパス)/(ご自身のアプリ名)/tmp以下のコマンドでどのユーザーがdbフォルダを使えるか確認します。
$ ls -la現在ログインしているLinuxユーザーの名前が表示されていなければ、そのユーザーに以下のコマンドで権限を与えます。
$ sudo chown -R (Linuxユーザーの名前) . password for (Linuxユーザーの名前):sudoコマンドなので、パスワードの入力が求められます。ここまで行けば、"docker-compose build"や"docker-compose up"など主要なDockerコマンドは問題なく実行できるはずです。
注:
似たようなエラーで、私は"FATAL: could not open file “
global/pg_filenode.map”: Permission denied"というエラーをよく見たのですが、こちらは"docker-compose stop"→"docker-compose up"でリスタートすれば大丈夫です。2 No space left on your device
Dockerfileの設定によって、Dockerイメージの容量が大きくなってしまった場合に起きるエラーです。不要なイメージ並びにコンテナは以下のコマンドで簡単に削除できます。
$ docker image prune $ docker container pruneこれらのコマンドを入力すると、以下のメッセージが表示されます。
WARNING! This will remove all dangling images (もしくはcontainers). Are you sure you want to continue? [y/N]dangling imagesは不要なイメージという意味なので、"y"を入力してください。容量が確保され、PermissionError解決時と同様の主要なDockerコマンドを実行できるようになっているはずです。
もしもさらに削除する必要がある場合は、以下で削除するイメージを探します。
$ docker images消したいイメージのIDを確認した後、以下で削除します。
$ docker image rm -f (消したいイメージのID)参考文献
英語ですが、この本はRuby on RailsのプロジェクトでDockerを使う方法をかなり詳細に論じているので、おすすめです。
Docker for Rails Developers: https://pragprog.com/titles/ridocker/
残念ながら、英語も含めてRails用のDockerの資料はまとまっているものが少ないので、特にRails歴が浅く、Dockerについての易しい資料が読みたいということであれば、英語ですが読む価値はあると思います。
Mediumの英語の記事
本テーマに関してより詳細に、Mediumに英語で記事を書いたので、そちらもご覧になってみてください。
- 投稿日:2020-07-15T20:36:32+09:00
そうだ、画像をプレビューしよう。~part5~
前提
- ruby on rails 6.0.0 を使用。
- ユーザー機能はdeviseにより導入されているものとする。
- viewファイルは全てhaml形式とする。
- ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。
はじめに
前回(part4)のあらすじです。複数画像を含んだ商品情報の登録、編集機能までが実装できました。ひと通りの作業は終わった感じですね。
ちなみに前置きや手順などは part1 に詳しく記載してあるので気になったら是非見てやってください。
さぁ!残るはプレビューのみだ!
プレビューします
それではまずjsから記述していきます。
やることしてはフォームのときと似たような感じです。html生成用の関数を作って、新しいフォーム同時に表示させます。画像を削除した場合はプレビューも消えるようにしましょう。
app/assets/javascripts/product.js$(function() { // ~省略~ const buildImg = (index, url)=> { const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`; return html; } // ~省略~ $('#image-box').on('change', '.file', function(e) { const targetIndex = $(this).parent().data('index'); const file = e.target.files[0]; const blobUrl = window.URL.createObjectURL(file); if (img = $(`img[data-index="${targetIndex}"]`)[0]) { img.setAttribute('src', blobUrl); } else { $('#image-box').append(buildFileField(fileIndex[0])); fileIndex.shift(); fileIndex.push(fileIndex[fileIndex.length - 1] + 1) } }); // ~省略~ $('#image-box').on('click', '.remove', function() { // ~省略~ $(`img[data-index="${targetIndex}"]`).remove(); }); });長くなってしまいましたがこんなところです。複雑なのは真ん中の部分だけなのでそう身構えることもないと思います。
それでは順番に解説をつけていきましょう。
const buildImg = (index, url)=> { const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`; return html;まずはhtmlを生成するための関数ですが、特に難しいことは何もないので大丈夫だと思います。引数として渡されたurlで、大きさを指定しながら画像を表示しています。
$('#image-box').on('change', '.file', function(e) { // フォームに割り振られた固有のインデックスを取得。 const targetIndex = $(this).parent().data('index'); // 画像ファイルのweb上におけるURLを取得。 const file = e.target.files[0]; const blobUrl = window.URL.createObjectURL(file); // 該当するインデックスを持つ画像の有無で条件分岐 if (img = $(`img[data-index="${targetIndex}"]`)[0]) { // 前行で取得した画像のURLを差し替える。 img.setAttribute('src', blobUrl); } else { $('#previews').append(buildImg(targetIndex, blobUrl)); $('#image-box').append(buildFileField(fileIndex[0])); fileIndex.shift(); fileIndex.push(fileIndex[fileIndex.length - 1] + 1)次に画像を選択した際の処理です。内容としてはこんな感じ。
難しいのは5,6行目のURLの取得ですが、ここに関してはいまいち自分でも理解できていないので後ほど詳しく調べようと思っています。動作自体はこれでうまくいくのでひとまず安心してください。
ちなみにelse以降の部分は元々あった記述にプレビュー表示を追加しただけなので詳しくは前のpartをご覧くださいな。
こいつらをelse以下に移動した理由としては、今までの状態では画像を差し替えるだけでも新しいフォームが表示されてしまっていたためです。なのでこいつらをelse以下に置くことで、画像追加時のみフォームが追加されるようにしてます。
$(`img[data-index="${targetIndex}"]`).remove();最後はこいつですが、ただ削除ボタンに合わせてプレビューを削除しているだけです。ほんとにそれだけ。
これでjsの処理が完成したので、今度は編集画面にあらかじめ表示されるべきプレビューを追加していきます。
app/views/products/_form.html.haml= form_with model: @product, local: true do |f| = f.text_field :name, placeholder: 'name' #image-box #previews - if @product.persisted? - @product.images.each_with_index do |image, i| = image_tag image.src.url, data: { index: i }, width: '100', height: '100' = f.fields_for :images do |i| // ~省略~ = f.submit 'SEND'追記したのは @product.persisted? の部分です。productに紐づいた画像をurlで1枚ごと取り出し、image_tagを用いて表示させています。
ちなみに.each_with_index とは、引数を二つ設定することで、.eachと同時に一つずつ番号を割り振るメソッドです。
さて、これでプレビュー機能も実装完了となります。
ということは、最後に
遂に完成です!!結構な時間がかかりましたが、なんとか終えることができました。ふぅ。
完成とは言いつつもできたのは機能面だけなので、あとはゆっくりマークアップをしていく感じになります。
とはいえ仕様書の内容は全て実装することができました。実際のところ大して難しいことをしてるわけでもないのに長々と書いてしまってすみません。
いないとは思いますが、最後まで読んだ方がいらっしゃいましたら長らくどうもありがとうございました。何かの参考になれば幸いです。
- 投稿日:2020-07-15T19:28:26+09:00
rubyのバージョン上げてrails sしようとしたらLibrary not loaded
エラーの抜粋
/Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require': dlopen(/Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
Referenced from: /Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle
Reason: image not found - /Users/hoge-user/projects/freee-auth/vendor/bundle/ruby/2.6.0/gems/mysql2-0.4.10/lib/mysql2/mysql2.bundle
Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
と言っているのでbundle doctorThe following gems are missing OS dependencies: * mysql2: /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib * mysql2: /usr/local/opt/openssl/lib/libssl.1.0.0.dylibmysqlが依存しているopenssl系の何かが足りないと言っている。
rubyのバージョンアップに伴ってopensslを入れ替えたからか。
ということでmysqlをinstallしなおして依存しているgemライブラリを入れ直すbundle exec gem uninstall mysql2 bundle install # mysqlのgemも一緒に入るはずrails s すると正常に動いた
$ bundle exec rails s -p 3099
=> Booting Puma
=> Rails 5.2.4.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 4.3.1 (ruby 2.6.6-p146), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3099
* Listening on tcp://[::1]:3099
- 投稿日:2020-07-15T19:16:51+09:00
未経験初学者がRails Serverを立てられなくて困ってた話。
はじめまして。たくまです。
自分なりに困ったことがあり解決ができた経験をアウトプットしてみようと思います。経緯
ProgateのWeb開発パスを勧めていくうちにうまくいかない箇所が出て来たので自分で行った解決法を共有しようと思います。
理解が浅くフワッとした内容なのでご指摘があればよろしくお願いします。問題
Web開発パス(Ruby on Rails)のSTEP4、[Ruby on Railsの環境構築をしてみよう!(macOS)]では最終的にはブラウザにてRailsサーバを立てて終わりになります。
しかし、エディタにコマンドを入力しても立てることができない!!
具体的には~ Could not find a JavaScript runtime. ~といった内容が表示されブラウザにRailsの表示ができませんでした。
ターミナルをいじる経験もあまりなかったため、エラーコードをググってみると下記のような記事が見つかりました。解決
どうやらJavaScriptに関することを言っているようです。
そこでエラーコードにあった"therubyracer"についてググってみると以下のような記事が見つかりました。開発環境構築に関して知識のなかった自分としてはこの記事を見ることにより落ち込まずに学習を続けることができました。
開発環境構築が鬼門な点だ。rubyにはrubygemsというライブラリの管理システムがある。Railsはたくさんのライブラリ(gem)を起動前にインストールする必要があるのだが、これが理不尽なエラーとともに失敗することが頻発する。慣れると対応策は分かってくるのだが、初心者だと「立ち尽くすしかない」と思う。
内容としてはtherubyracerという部品が古くなっており、新しくmini_racerという部品が広く普及していると言った内容。
→なんとなくAppleのSidecarと乗り物繋がりでフロントよりの技術てことでJavaScriptと関係あるかもなんて思ってた。実施
改めてレッスンで作成した sample_app < Gemfile 内容を確認するとmini_racerがコメントアウトされてる...
こいつのコメントアウトを外してあげて編集点を保存してくれる?らしい"bundle update"を実施しアップデート。
後に"Rails s"を実施すると無事にブラウザでServerを立ち上げることができました。あとがき
今回、はじめて問題点に関する解決法を記事にしてみましたが、gemの内容や自分と一緒の場所でつまずいている方が仮にこの記事を参考にしてくれた場合の動線の意識など反省点がや改善点が多々ありました。
今後もこのようなアウトプットを続けて行こうと思います。
- 投稿日:2020-07-15T18:34:50+09:00
高速なのは Range#cover? or Range#include? or ActiveSupport::TimeWithZone#between? それとも?
rubocop-performance
を確認していたら、Performance/RangeInclude: Use Range#cover? instead of Range#include?.
が出たのでベンチマークを取ってみました。
require 'bundler/setup' require 'benchmark_driver' Benchmark.driver do |x| x.prelude <<~RUBY require 'active_support/time' t = Time.current + 5.minutes RUBY x.report 'include?', %[ (Time.current..Float::INFINITY).include?(t) ] x.report 'cover?', %[ (Time.current..Float::INFINITY).cover?(t) ] x.report 'between?', %[ t.between?(Time.current, Float::INFINITY) ] x.report 'raw_function', %[ t >= Time.current && t <= Float::INFINITY ] end結果
Warming up -------------------------------------- include? 85.071k i/s - 86.456k times in 1.016282s (11.75μs/i) cover? 131.933k i/s - 135.600k times in 1.027793s (7.58μs/i) between? 276.108k i/s - 285.615k times in 1.034434s (3.62μs/i) raw_function 285.200k i/s - 291.396k times in 1.021725s (3.51μs/i) Calculating ------------------------------------- include? 125.985k i/s - 255.212k times in 2.025732s (7.94μs/i) cover? 133.774k i/s - 395.799k times in 2.958713s (7.48μs/i) between? 304.505k i/s - 828.322k times in 2.720227s (3.28μs/i) raw_function 304.348k i/s - 855.600k times in 2.811255s (3.29μs/i) Comparison: between?: 304504.7 i/s raw_function: 304348.1 i/s - 1.00x slower cover?: 133774.0 i/s - 2.28x slower include?: 125985.1 i/s - 2.42x slower
include?
からcover?
に変更したほうが早いが、between?
や便利機能使わずに普通に書いたraw_function
の方が早くなりました。 後者の2つは実行するタイミングで順位がわかりましたが、ほぼ誤差の範囲だと思います。ちなみに
benchmark_driver
では、 ymlで入力することもサポートしているので、# range.yml prelude: | require 'bundler/setup' require 'active_support/time' t = Time.current + 5.minutes benchmark: include?: (Time.current..Float::INFINITY).include?(t) cover?: (Time.current..Float::INFINITY).cover?(t) between?: t.between?(Time.current, Float::INFINITY) raw_function: t >= Time.current && t <= Float::INFINITYのようなファイルを用意して、
$ benchmark-driver range.yml
と書いても似たような結果を得られました。
- 投稿日:2020-07-15T18:24:04+09:00
【form_withのmodel:の意味】
form_withに記述するmodel:の意味
いまいち理解できていなかったのですがようやく意味がわかったのでアウトプットとして記述していきます。
上の画像はツイートの詳細画面からコメントを投稿するためのフォームを表示させるためのコードを記述しているのですが、
form_withに続くmodel:@tweet, @commentの二つをなぜ使用しているのかいまいち理解できていませんでしたが解決できましたので以下に理由を記述します。1点目
今回の場合はツイートに対してコメントをするという状況でありコメントは一つのツイートに紐付いている状態なのでコメント単体でフォームの送信先に指定することはできないため@tweet, @commentと紐付いているものとセットにして送信先を指定しているという理由。
2点目
@tweet,@commentという記述自体がルーティングを指定しているということ。
どういうことかといいますと、以下の画像を見てください。今回の場合はコメントを作成したいので発動させたいアクションはcommentsコントローラーのcreateアクションです。
そして一番左のprefixの項目を見ると、tweet_commentsというパスが記述されています。
要するにフォームを送信してコメントを作成するのであればこのルーティングを指定しなければいけないということです。そのルーティングを指定しているのがmodel: [@tweet,@comment]だというのが二点目の理由です。
以上です。
- 投稿日:2020-07-15T18:05:34+09:00
Unicornを再起動する
前提
RailsアプリをEC2にデプロイし、Unicornをアプリケーションサーバーとして使用しています。
開発環境でしたら、
rails s
で起動すれば良かったのですが、本番環境はサーバーの起動&停止方法が変わったので、メモとしてまとめました。サーバーを停止したい場合の手順
Unicornの起動確認
ps -ef | grep unicorn | grep -v grepvinaka 15533 1 0 08:02 ? 00:00:01 unicorn_rails master -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production vinaka 15537 15533 0 08:03 ? 00:00:00 unicorn_rails worker[0] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production vinaka 15538 15533 0 08:03 ? 00:00:00 unicorn_rails worker[1] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production
master、worker[0]、worker[1]
の
三つ出てきた場合、Unicornが起動しているようです。killする
今確認した、三つのうちの一番上の番号(master)
15533
をkillします
(毎回番号が変わるので都度ps -ef | grep unicorn | grep -v grepで番号を確認)kill -9 15533Unicornの停止確認
ps -ef | grep unicorn | grep -v grepすると何も表示されないはず。
表示されていないと、Unicornが停止しています。再度サーバーを立ち上げる時の手順
Unicornの停止確認
ps -ef | grep unicorn | grep -v grepUnicorn起動!
bundle exec unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E production何も出ないと、ちゃんと起動しているようです。
一応確認もする。
ps -ef | grep unicorn | grep -v grepvinaka 15740 1 1 08:48 ? 00:00:01 unicorn_rails master -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production vinaka 15744 15740 0 08:48 ? 00:00:00 unicorn_rails worker[0] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production vinaka 15745 15740 0 08:48 ? 00:00:00 unicorn_rails worker[1] -c /var/www/rails/ShitsumonWa-/config/unicorn.conf.rb -D -E production三つ表示されました!起動されている。
さっきと番号が変わっているはず!ちなみに
master failed to start, check stderr log for detailsとエラーが出た場合は
cat log/unicorn.logでエラーの内容を確認する。
Unicornのバージョン(5.5以上だとエラーが出るみたいなのでGem file
でバージョン指定するのが良いです。)Gemfilegroup :production, :staging do gem 'unicorn', '5.4.1' endまた
unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E productionだと怒られることがあるので、
bundle exec
をつけてあげた方が無難。bundle exec unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E productionnginxも再起動
Unicorn
が無事に立ち上がったら、nginxも再起動してあげて完了です。sudo nginx -s reload終わりです。
- 投稿日:2020-07-15T17:48:13+09:00
rbenvの使い方について
今回、伊藤淳一さんのRuby入門を購入し、学習を進めるにあたって
Rubyのバージョンを変更する必要があったため、rbenvについて調べてみました。rbenvとは
Rubyのバージョンを切り替えてくれるツールです。
まずは、rbenvをインストールしたいと思います。rbenvのインストール方法
ターミナル# 初期化設定 $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile # 初期化設定の反映 $ source ~/.bash_profile # rbenvのインストール $ brew install rbenv ruby-buildこちらでインストール完了です。
次に、指定したRubyのバージョンをインストールする方法を記載します。指定したRubyのバージョンをインストールする方法
ターミナル# インストール可能なRubyのバージョン一覧を表示する $ rbenv install -l # 一覧からインストールしたいRubyのバージョンをインストール $ rbenv install 2.4.1 # インストールしたRubyのバージョンを使用可能な状態にする $ rbenv rehash複数のバージョンをインストールしたい場合は、上記を繰り返すことで可能。
rbenvのフォルダ構成
~/.rbenv/ (ルートフォルダ)
~/.rbenv/shims/ (rubyやgemがインストールしてくれるコマンドのラッパーを保存するフォルダ)
~/.rbenv/version (グローバルに設定しているRubyバージョンを記録するファイル)
~/.rbenv/versions/ (その他のインストールした各種Rubyバージョンをインストールするフォルダ)引用元:rbenvとは - Qiita
使用するバージョンを指定
ターミナル# PC全体で使用するRubyのバージョンを切り替える $ rbenv global 2.4.1 # インストールできたか確認 $ ruby -v参考にした記事
- 投稿日:2020-07-15T17:23:12+09:00
Progate Ruby on Rails5 振り返り
プロゲート Ruby on Railsコース所感
プログラミングの学習に200時間ほど費やし、Railsコースに着手しました。
今までの学習ではスムーズに進んでこれたものの、今回は苦戦。
道場コース4の内容は完全には理解できていません。
復習できるように、メモとして記録を残します。
マイグレーションファイル関連
マイグレーションファイルとは:データベースを作成するときの設計図。
モデルとは:テーブルを操作するための特殊なクラス
モデルの作成rails g model Post content:textrails g model モデル名 カラム名:データ型
※モデル名は単数系で指定する。
マイグレーションファイルの作成rails g migration add_image_nameURLから値を取得する
ルーティングでハッシュを指定する。
routes.rbget "posts/:id" => "posts#show"params[ハッシュ]とすることで取得できる
controller.rbdef show @id = params[:id] end※params[:~~]で取得できる値は文字列であるため、比較等を行う場合には整形すること。
フォームの送信先の設定
posts/new.html.erb<%= form_tag("/posts/create") do%> <% end %>doを忘れないこと
post時のリンク記述方法
第三引数としてメソッドのpostを指定する。
<%= link_to("削除する","/posts/#{@post.id}/destroy",{method: "post"})%> <% end %>データベースへの保存処理での分岐
if ~~~の部分で保存処理は実行されていることに注意
(真偽値を返すだけではない)def update @post = Post.find_by(id: params[:id]) if @post.save redirect_to("/posts/index") else redirect_to("/posts/#{@post.id}/edit") end endデータベースを経由せず、直接ビューに表示する(render)
render(コントローラー名/ビュー名)と指定する
def update @post = Post.find_by(id: params[:id]) if @post.save redirect_to("/posts/index") else render("posts/edit") end end画像の送信欄
typeの指定を忘れないこと
<input name="image" type="file">画像の送信フォーム
{multipart: true}を指定する
<%= form_tag("~~~",{multipart: true}) do%>送信された画像の保存処理
受け取ったparams[:image]に対して、readメソッドを活用し画像データの中身を取得する。
File.binwrite("ファイルの場所","ファイルの中身")def update @user = User.find_by(id: params[:id]) @user.name = params[:name] @user.email = params[:email] @user.image_name = "#{@user.id}.jpg" image = params[:image] if params[:image] File.binwrite("public/user_images/#{@user.image_name}",image.read) end endセッションの設定
ページを移動しても、ユーザー情報を保持し続けるために、sessionという特殊な変数を活用する。
sessionに代入された値はブラウザに保存される。session[:user_id] = @user.id共通変数の定義
application.html.erbは全アクションから呼び出される。
そのため、全アクションで活用する変数を定義すると効率が良い。
before_action :アクション名 とすることでアクションが呼び出される前に「アクション名」が実行される。application.rbbefore_action :set_current_user def set_current_user @current_user = User.find_by(id: sessino[:user_id]) end投稿テーブルに持たせたuser_idから別テーブルに存在するユーザー情報を取得する
使用するデータが存在するテーブル(ここではPostモデル)に対してアクションを定義する
post.rbdef user return User.find_by(id: self.user_id) end活用例(投稿から、それに紐づくユーザーを取得する)
posts_controller.rbdef show @post = Post.find_by(id: params[:id]) @user = @post.user endユーザーテーブルに持たせたidから別テーブルに存在する投稿情報を取得する
使用するデータが存在するテーブル(ここではUserモデル)に対してアクションを定義する
user.rbdef posts return Post.where(user_id: self.id) end活用例(ユーザーから、それに紐づく投稿を全て取得し表示する)
show.html.erb<% @user.posts.each do |post|%> <img src="<%= "/user_images/#{post.user.image_name}" %>"> <%= link_to(post.user.name, "/users/#{post.user.id}") %> <%= link_to(post.content, "/posts/#{post.id}") %> <% end%>各ユーザーがいいねした投稿の一覧表示
ルーティングの指定
routes.rbget "users/:id/likes" => "users#likes"アクションの定義
users.controller.rbdef likes @user = User.find_by(id: params[:id]) @likes = Like.where(user_id: @user.id) endビューでの表記
likes.html.erb<% @likes.each do |like|%> <% post = Post.find_by(id: like.post_id)%> <img src="<%= "/user_images/#{post.user.image_name}" %>"> <%= link_to(post.user.name, "/users/#{post.user.id}") %> <%= link_to(post.content, "/posts/#{post.id}") %> <% end%>二行目のLikeテーブルのpots_idを用いて、対応する投稿を取得する部分を忘れないように。
- 投稿日:2020-07-15T16:52:03+09:00
「商品画像を差し替える」と心の中で思ったならッ!その時スデに行動は終わっているんだッ!~part4~
前提
- ruby on rails 6.0.0 を使用。
- ユーザー機能はdeviseにより導入されているものとする。
- viewファイルは全てhaml形式とする。
- ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。
はじめに
前回(part3)のあらすじです。jQueryを使用して新規登録画面にあんなことやこんなことを実装しました。
現在の状態としては、画像を選択すると新しいフォームが出現する、削除を押したら消える、という感じです。一見出来上がったように見えますが、これらは全てjsによるアクションなのでリロードされた編集画面では消えてしまいます。
なので今回のpartでは、編集画面にも新しいフォームを出現させる、といった作業になります。
ちなみに前置きや手順などは part1 に詳しく記載してあるので気になったら是非見てやってください。
やってみよう
やることとしては主に以下の3つです。
- 編集画面に新しいフォームを表示させる。
- 削除ボタンで、すでに登録された情報も削除できるようにする。
- 画像フォームがもつ固有のインデックスが被らないようにする。
こんな感じですね。さっそくやっていきますが、ビューファイルをいじる前にコントローラに少し追記をする必要があるので、そちらからいきましょう。
app/controllers/products_controller.rb#~省略~ private def product_params params.require(:product).permit(:name, images_attributes: [:src, :_destroy, :id]).merge(user_id: current_user.id) end #~省略~ endストロングパラメータに少し記述を増やしました。この_destroyというのは、関連づいた子モデルの情報を削除してくれるキーです。見慣れない形ですがちゃんとしたキーなので安心しましょう。
それではビューファイルを記述していきます。
app/views/products/_form.html.haml= form_with model: @product, local: true do |f| = f.text_field :name, placeholder: 'name' #image-box // ~省略~ - if @product.persisted? .group{ data: { index: @product.images.count } } = file_field_tag :src, name: "product[images_attributes][#{@product.images.count}][src]", class: 'file' .remove 削除 = f.submit 'SEND'追記したのは@product.persisted?の部分です。難しく見えますが、やっていることとしては前回jQueryで追加したフォーム部分をhaml形式にしただけです。新しいフォームを表示させようってやつですね。
このpersisted?ですが、こいつは利用したインスタンスが保存済みか否かを判断してくれます。要は新規なのかすでに登録された情報なのかってことです。とても便利なので覚えておきましょう。
表示はできましたが、現在の削除ボタンではすでにデータベースに登録された情報を削除することができません。なので先ほど追加した_destroyキーを使って削除できるようにする必要があります。
app/views/products/_form.html.haml= form_with model: @product, local: true do |f| = f.text_field :name, placeholder: 'name' #image-box = f.fields_for :images do |i| // ~省略~ - if @product.persisted? = i.check_box :_destroy, data: { index: i.index }, class: 'hidden' - if @product.persisted? // ~省略~ = f.submit 'SEND'先ほどとは別に、image-boxの内部にもpersisted?で文を追加しました。_destroyキーを持ったチェックボックスにチェックを入れると、データベース上から対応するレコードが削除される、といった記述です。なぜそうなるのかは詳しく書きませんが(自分が理解しきれていないので...)、こういう書き方があるのか程度に覚えておくといいと思います。
さて、これで仕組み自体はできました。ですが削除ボタンと別にチェックボックスがあるのはよろしくないのでこいつらを連動させていきます。
app/assets/javascripts/product.js$(function() { // ~省略~ $('.hidden').hide(); $('#image-box').on('click', '.remove', function() { // フォームに割り振られた固有のインデックスを取得。 const targetIndex = $(this).parent().data('index') // 取得したインデックスに対応するチェックボックスを取得。 const hiddenCheck = $(`input[data-index="${targetIndex}"].hidden`); // チェックボックスが存在する場合チェックを入れる。 if (hiddenCheck) hiddenCheck.prop('checked', true); ~省略~ }); });image-boxのクリックイベントに処理を追加しました。一行ずつの解説も書いておきました。やっていること自体は簡単なので、jQueryの基礎が分かっていれば問題はないと思います。
削除ボタンを押せばチェックもできるようになったのでチェックボックスは非表示にしておきましょう。今回はjsの.hide()を使用してやりましたが、cssで display: none にしていただいても構いません。
それでは最後にインデックス被りを防止して実装は完了となります。
app/assets/javascripts/product.js$(function() { // ~省略~ let fileIndex = [1,2,3,4,5] lastIndex = $('.group:last').data('index'); fileIndex.splice(0, lastIndex); // ~省略~ });fileIndexの定義部分に記述を追加しましょう。考え方としては、現在使われている最後のインデックスを取得し、その値でfileindexの値を入れ替える、といった感じです。
ちなみに.spliceについてですが、こいつは指定した要素から数えて好きな分だけ取り除き、ついでに要素の追加もできてしまう優れものです。
今回は第一引数で指定した数以降の値を全て取り除き、第二引数で指定した値を挿入してくれています。やれることが多い分、書き方に多少複雑な部分があるので是非詳しく調べてみてください。
さて、以上で編集機能自体の実装は終了となります!
最後に
とうとう出来上がりましたね。いやぁ長かった。
仕様書でいう「画像をの差し替えを一枚ごとにできる。」は達成できたとしていいと思います。あとはわかりやすいように画像のプレビューをつければ完成です。
アプリで画像をプレビューする前に記事に画像をプレビューしろよって天の声が聞こえますが気にしないことにします。読みづらくてすみません。
それではまた次のpartで!!
- 投稿日:2020-07-15T16:02:45+09:00
分かりやすいプログラムを書くために意識する事8選!!
・ "分かりやすいプログラム"とはどんなの?
・ 何に気を付けて書けば"分かりやすいプログラム"になるのだろう?プログラミングを進めていくと"分かりやすいプログラムが重要だよ!"と言われることがあると思いますが、最初はいまいちピンとこないですよね。
今回は「勉強会に参加して学んだ事」や「オリジナルアプリをレビューしてもらった際に学んだこと」を元に、自身も日々意識すべき内容として分かりやすいプログラムについて気を付けるPoint8選としてまとめていこうと思います。
分かりやすいプログラムとは
結論: コードを読む人を意識する
数ヶ月前の自分のコードを見た時に「読みにくいな..誰がこんなコード書いたんだ?!」とならないようにしたいですね。
そして、チーム開発する場合においても読みにくいコードだとチームのメンバーにも迷惑をかけたり、開発する上で誤解が生じやすくなります。
常に読み手を意識してコードを書く習慣をつけておくのは大切だと思います。ここからは、具体的に何を学んで、何を意識するべきか書き出していこうと思います。
(基本的な内容もありますが過去の自分で感じた事は、他の方の参考にもなると思って載せます)Point1: ファイルの分割
main.rbrequire "./messages" require "./controller" class Game def initialize(style, count, data) @style = style @count = count @data = data end end注目箇所:
ファイルがmain.rb
にも関わらずGameクラス
が入っていますね。このままだと、
main.rb
を見たときに「ん??mainファイルにGameクラス?これには何か意味があるのかな?」と疑問に思ってしまいます。main.rb内でGameクラスを定義しなくてはいけない理由も無く、Gameクラスだけで使用することを考えるとファイルを分けた方が分かりやすいですよね。
Point2: コメントとして書くべき内容
main.rb# コントローラーを生成 controller = Controller.new注目箇所:
controller = Controller.new
のコメントが「コントローラーを生成」
..そのままです。無駄なコメントはもちろんですが、コメントの多すぎもよくありません。
人によってはコメント肯定派と否定派がいるようですが、書くべき状況とすれば
そのコードがどういう事を意図しているかなど、コードだけでは読み取れない内容に対して書くことを意識してみるといいと思います。Point3: 分かりやすいメソッド名にする
分かりやすいメソッド名というのは、英語を読むかの如く命名することが大切です。
下記が参考例です。game.rbrequire "./messages" require "./controller" class Game def initialize(style, count, data) end def question_style # 各問題に対する処理 end end前提条件:
game.question_style
を行うと、クイズの問題定義の形式を振り分けた上でゲームの処理が開始する状況の場合です。
注目箇所:ゲームプログラムにおいて
question_style
というメソッド名を見た時、皆さんは何の処理をしているメソッドだと思いますか?質問の様式を決めている?..質問の様式を選択してる?..
少なくともメソッド名を見ただけでは、"各問題に対する処理が入っている"とはわからないですよね。つまり、
中のコードを読んだ時に初めて何の処理を行うメソッドなのかがわかるメソッド名="分かりにくメソッド名"になってしまいます。メソッド名が
question_style
だと、コードを書いた側目線!
"問題定義の形式を決めて回答処理を切り替えるようにした"という実装の都合になってしまいます。今回の例でいうと、メソッド名は
start
やplay
などを使うとどうでしょう?
ゲームプログラムにおいてstart
というメソッド名は"ゲームをスタートするメソッド"なんだな。と分かりますね!メソッド名で表現するのが難しい場合もあると思います。
どうしても必要なメソッドで名前だけでは分かり難い場合は、コメントをうまく使うのも有りだと思いますが可能な限りメソッドの中で何が行われるのかをメソッド名とパラメータを使って読み手が分かるようにしていきましょう。補足ポイント:メソッドを命名する場合は動詞を意識しよう!
Point4: 分かりやすい変数名にする
メソッドの命名と被りますが、
分かりやすい変数名というのは、英語を読むかの如く命名することが大切です。変数名は仮の入れ物だと思って適当に命名すると、何の情報が入っているのか全く伝わらなくなってしまいます。
「番号が入っている変数なんだからnumber
だろう!」と安易に決めないようにしましょう。
仮に変数名をnumber
とした時に何の番号なのか?が分かりません。
データの番号? ユーザーの番号? 配列内の要素の数かも?変数名を見た瞬間に何の情報が入っているのか分かる名前をつけることは難しいです。
現場のエンジニアの方の対談で聞いた際も、「メソッド名や変数名には悩む時もある。」とおっしゃっていました。
それほど、命名は重要だということですね。補足ポイント:変数を命名する場合は名詞を意識しよう!
スコープを意識しよう。
ここではスコープに関しても詳しく書くことはしませんが他の方の記事を載せておきますね!
スコープに関しての参考記事: Qiitaスコープは簡単にいうと変数の有効範囲になります。その範囲を超えるとその変数は使えません。
有効範囲を理解してコードを書かないとバグや不具合にも直結してしまいます。
「変数名の大切さはわかった..
とは言え、どうしても良い変数名が思いつかない場合はどうしたらいいでしょうか?」そんな時は、変数にしたい内容を日本語で表現して、それを変数名に変換してくれるサイトがあるのでそちらを使ってみるのも良いかもしれません。
それがプログラマーのためのネーミング辞書 codicです。
参考サイト: codicへPoint5: メソッドや変数の命名の際、単語数は2単語..程度
フワッっとした回答になってしまい申し訳ないのですが..
前提:言語によって違いがあるようです。
iPhoneのアプリ開発だと補完してくれたりするので長くなるようです。
今回はRubyの場合で説明します。
注意: 以下、状況にもよると思いますので参考にして下さい。・ 単語は極力短い方が読みやすい(短くて伝わらなければ本末転倒ですが..)
・ 命名したその変数を使う箇所が近く(メソッド内や次の行で使うもの)であれば短くても良い
・ ローマ字にならないように2単語の例:
game_data
みたいな形です。Point6: コードを書く!!!
体系的な感じになりますが、やっぱり自分で書きまくる!かと思います。
人のコードを読めるようにするためにも、自分が書きまくる!
もう..書くしかないです。Point7: 人のコードを読む!!!
Githubに公開されているものもたくさんあります。
正直なにがなんだかわからないことも多いですが、自分の制作しているものに似ている物もあると思うので、検索してみましょう!
私自身が色々見て学びたいけど、公開されているコードなんてどうやって探すんだ?という時期があったので載せておきますね!Githubでコードを探す: Github search
こんな書き方あるんだ! この省略の仕方いいな! こっちの書き方の方が見やすいな!
など発見があると思います!Point8: コードのレビューをしてもらう!!!
知り合いやスクール、オンラインサロン。レビューしてもらえる環境があれば最高です!
他の方にレビューしてもらうのは恥ずかしい気持ちも無くはないですが、環境があればお願いしてみましょう!自分は独学だし、知り合いにエンジニアいないし..
そんな方は、GitHubを活用しましょう!
自分のコードを公開すると、他人がコメントやフォークを行うことができますよ!チームで開発することは必ずあると思います。
その際、GitHubは必要になってくるツールなのでこれを機にやってみるのもいいと思います。Github: Github
あとがき
少しでも参考になれば嬉しいです!!
プログラミングは苦しいこともたくさんありますが..汗
やっていて本当に楽しいですね!
楽しんで頑張って継続しましょうね!!
- 投稿日:2020-07-15T15:49:46+09:00
ターミナルでrailsサーバーを切らずに起こった「A server is already running...」の対処法
ローカルのVisual Studio Codeのターミナルをいじっていた時、PCの不具合で強制終了しました。
その後もう一度Railsサーバーを立ち上げたところ、
A server is already running.
と出てきました。
サーバーが稼働時の正常なシャットダウンは「ctrl + C」でOKですが、このケースだとこのコマンドを実行しても何も反応が有りませんでした。
ここでの対処法は2つあり、
1.
kill -9 $(lsof -i tcp:3000 -t)
を実行
2.PCを再起動私は1.を選択して、とりあえずサーバー接続を白紙にすることが出来ました。
補足(localhost:3000にアクセスできない問題)
「あぁとりあえず安心」と思って再度サーバーを接続したら、
yuichi kanban % rails s
=> Booting Puma
=> Rails 5.2.4.3 application starting in development
=> Runrails server -h
for more startup options
Puma starting in single mode...
* Version 3.12.6 (ruby 2.6.6-p146), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stopと出てくるものの、ブラウザで
localhost:3000
を検索しても読み込まれませんでした。こういった場合は上記の対処法に似ていますが、Visual Studio Codeを一旦閉じて再度起こしたら、無事にYay!You're on Rails!が表示されました。
詰まったときには再起動すれば解決することが多いと思うので試してみてください。(適当)
- 投稿日:2020-07-15T15:29:48+09:00
【Rails】Capybaraを使った統合テスト(導入〜簡単なテスト実行まで)
はじめに
統合テストとはアプリ上で意図したとおりの動作をしてくれるかを確認するテストのことです。
ブラウザ上で自分で行なっても良いですが、テストコードを書くことで大きなメリットがあるのでできれば書いておくと良いかと思います。メリット
・一度コードを書いてしまえばテストが簡単にできる
(テストコードがないとフォームの入力やリンクの移動など毎回行なう必要がある)
・思考を整理できる
書き下ろすことで自分の思考を客観視でき、抜け漏れに気付きやすくなる
・記録として残せる
アプリの動作を自分がどこまで想定しているか見返すことができる
また、第3者がテスト内容を用意に確認できる今回は以下のGIFファイルのように、投稿する動作がうまく行くかどうか確認してみようと思います。
Capybara導入
まずはCapybaraというgemを導入します。
Gemfilegroup :test, :development do # (省略) gem 'capybara' end※ このときtest環境の中にcapybaraが既に入っている可能性があるので、そのときはコメントアウトしておきましょう。rails5.2.3では入ってました。
ターミナル$ bundle installspec/rails_helper.rb# 以下の記述を追加 require 'capybara/rspec'次に作業ファイルを用意します。
spec/features
ディレクトリを作成しましょう。
その中にテスト用のファイルを作成します。
今回はpost_spec.rb
とします。テストコード
post_spec.rb
に記述していきます。spec/features/post_spec.rbrequire 'rails_helper' feature 'post', type: :feature do scenario '投稿ができること' do # 投稿ボタンが存在する visit root_path expect(page).to have_content('投稿') # 投稿処理が動作する expect { click_link('投稿') expect(current_path).to eq new_post_path fill_in 'post_content', with: 'こんにちは' fill_in 'post_tags_attributes_0_content', with: 'タグ' find('input[type="submit"]').click }.to change(Post, :count).by(1) # トップページにリダイレクトされる expect(current_path).to eq root_path # 投稿内容がトップページに表示されている expect(page).to have_content('こんにちは') end end一連の動作を
senario do 〜 end
の間に記述します。今回は投稿のリンクが表示されていることを確認し、投稿処理確認、リダイレクトの確認、投稿内容の表示確認を順番に行なっています。
visitで指定のパスへ移動、fill_inでフォームに入力、findやclickで投稿など各処理の詳細はここでは説明しませんが、調べると出てくるので自分がやりたい処理があれば探してみましょう。こちらのチートシートにcapybaraで使えるものが一通り書かれているので参考にしてみると良いかもしれません。
テスト実行
テストの実行は以下のようにファイルを指定して実行すると良いです。
ターミナル$ bundle exec rspec spec/features/post_spec.rbspec以下で実行したいファイルを指定します。
今回はfeaturesディレクトリのpost_spec.rbにテストコードを書いたので上のようになります。では実行してみます。
- 投稿日:2020-07-15T15:01:38+09:00
【N+1問題】
N+1問題とは
アソシエーションでモデル同士を関連づけている場合に起こる問題のこと。
例えばツイッターのようなアプリでユーザーと投稿が紐付いている場合に
トップページで投稿を一覧表示させる時に各投稿ごとに紐付いているユーザーを検索するために毎回毎回データベースにアクセスしているとアプリのパフォーマンスが下がってしまう。1万投稿ある場合は1万回データベースにアクセスしなければいけないということ。
この問題のことをN+1問題という。
この問題を解決するためのメソッドが
includeメソッド
と言われるメソッド。
このメソッドは毎回毎回データベースにアクセスして紐づくデータを検索していたところを、
一回で全データを取得してくれる。そのため1万投稿あるとしてもデータベースへのアクセス「は一度で済む。
上の画像のようにindexアクションの処理内容の箇所にincludes(:紐づくモデル)とすれば使用できる。
これでツイートなどを一覧表示する際に投稿がいくら増えたとしてもパフォーマンスを下げずに済む。
- 投稿日:2020-07-15T13:34:15+09:00
【Rails】Scopeの使い方
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina基本構文
class モデル名 < ApplicationRecord scope :スコープ名, -> { 条件式 } end使用例
下記のようユーザーのIDを降順にして5つだけ表示させたいと仮定します。
users_controller.rbUser.order(id: desc).limit(5)1.引数なし
models/user.rbclass User < ApplicationRecord scope :recent, -> { order(id: :desc).limit(5) } endusers_controller.rbUser.recent2.引数あり
models/user.rbclass User < ApplicationRecord scope :recent, -> (count) { order(id: :desc).limit(count) } endusers_controller.rbUser.recent(5)
- 投稿日:2020-07-15T12:57:52+09:00
railsアプリをawsでインフラ構築する際に出たエラー
今回はこちらの記事を参考に、始めて作成したrailsのappをAWSを使用してインフラ構築に挑戦した。
https://qiita.com/naoki_mochizuki/items/814e0979217b1a25aa3eEC2インスタンスの環境構築
$git make gcc-c++ patch
git: 'make' is not a git command. See 'git --help'.
The most similar commands are blame, merge, stageというエラーが出た。これはコメントを削除し、1行のコマンドとして実行する必要があった。
$sudo yum install git make gcc-c++ patch openssl-devel libyaml-devel libffi-devel libicu-devel libxml2 libxslt libxml2-devel libxslt-devel zlib-devel readline-devel mysql mysql-server mysql-devel ImageMagick ImageMagick-devel epel-release
$sudo mkdir www
mkdir: ディレクトリ `www' を作成できません: File exists
というエラーが出た。既に存在しているらしいが作成した記憶はない、、、
そのまま使用するか迷ったが、一応別名(XYZ)でファイルを作成して代用した。gitとの連携、アプリのクローン
$
cat aws_git_rsa.pub
cat: aws_git_rsa.pub: No such file or directoryこれは公開鍵を作成した際に通常のコマンド入力と操作が違ったため、:aws_git_rsaのうしろに半角スペースを付けてしまったのが原因だった。(凡ミス。。)
$ ssh-keygen -t rsa
Enter file in which to save the key ():aws_git_rsa(この部分に半角スペース)
鍵を再度作成したら無事に公開鍵の中身を出現させることができた。MySQLの設定
Mysqlの起動を試みたところ、次のエラーが発生
$sudo service mysqld start #mysqldの起動
Redirecting to /bin/systemctl start mysqld.service
Failed to start mysqld.service: Unit not found.
以下の記事を参考に次のコマンドを実行したところうまくいった。
https://qiita.com/yuta-38/items/4074f5ada9e22088c8dd
$ sudo yum install -y mariadb-server
$ sudo systemctl enable mariadb
$ sudo service mariadb start
また、Amazon Linux2ではyumでmysqlをインストールしようとするとmariaDBをインストールしようとするらしい。
私は実践しなかったが、以下の記事が参考になるかもしれない。
https://qiita.com/hamham/items/fd77bb0bb167a150dc8e#mysql57%E3%81%AE%E5%B0%8E%E5%85%A5Nginxの起動
以下のコマンドを入力したがNginxが起動しない。
$ sudo service nginx start
Redirecting to /bin/systemctl start nginx.service
Job for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details.エラーの指示通りに以下のコマンドを実行
$ systemctl status nginx.service
nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 月 2020-07-13 09:10:04 UTC; 57s ago
Process: 11360 ExecStart=/usr/sbin/nginx (code=exited, status=1/FAILURE)
Process: 11356 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 11355 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)原因が分からなかったので以下も実行。
$sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful設定ファイルに問題はなさそうだったので、困ったことになった。
色々調べているうちに以下のコマンドを発見。
$ sudo service nginx status -l
Redirecting to /bin/systemctl status -l nginx.service
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 月 2020-07-13 09:10:04 UTC; 27min ago
Process: 11360 ExecStart=/usr/sbin/nginx (code=exited, status=1/FAILURE)
Process: 11356 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 11355 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
7月 13 09:10:02 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
7月 13 09:10:03 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal nginx[11360]: nginx: [emerg] still could not bind()
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: nginx.service: control process exited, code=exited status=1
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: Failed to start The nginx HTTP and reverse proxy server.
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: Unit nginx.service entered failed state.
7月 13 09:10:04 ip-10-0-10-10.ap-northeast-1.compute.internal systemd[1]: nginx.service failed.上記によると、設定した80番ポートは既に起動中のようだ。
原因を考えた結果、以前あるweb講座でApacheのインストール及び自動起動設定をしていたのを思い出した。
$ sudo systemctl status httpd.service
● httpd.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
Active: active (running) since 日 2020-07-12 06:24:44 UTC; 2 days ago
Docs: man:httpd.service(8)
Main PID: 21760 (httpd)
Status: "Total requests: 149; Idle/Busy workers 100/0;Requests/sec: 0.00084; Bytes served/sec: 2 B/sec"
CGroup: /system.slice/httpd.service
├─20766 /usr/sbin/httpd -DFOREGROUND
├─21049 /usr/sbin/httpd -DFOREGROUND
├─21055 /usr/sbin/httpd -DFOREGROUND
├─21056 /usr/sbin/httpd -DFOREGROUND
├─21760 /usr/sbin/httpd -DFOREGROUND
├─21761 /usr/sbin/httpd -DFOREGROUND
├─21762 /usr/sbin/httpd -DFOREGROUND
├─21763 /usr/sbin/httpd -DFOREGROUND
├─21764 /usr/sbin/httpd -DFOREGROUND
├─21765 /usr/sbin/httpd -DFOREGROUND
└─21887 /usr/sbin/httpd -DFOREGROUND
案の定Apacheが既に動いていた。自動起動を無効&停止後に、NginXの起動コマンドを実行したら無事に起動できた。インフラ構築前にUdemyで購入した以下の講座が効いてテンポよく理解できた。
https://www.udemy.com/course/aws-and-infra/
- 投稿日:2020-07-15T12:01:41+09:00
クラスメソッド
クラスメソッドとは
クラスに関連が深いものの、一つ一つのインスタンスに含まれるデータを使わないメソッド
クラスメソッドを定義する方法1
class クラス名 def self.クラスメソッド #クラスメソッドの処理 end endクラスメソッドを定義する方法2
class クラス名 class << self def クラスメソッド #クラスメソッドの処理 end end endクラスメソッドを呼び出す場合は以下のようにする
クラス名.メソッド名class Food def initialize(name) @name = name end #self.をつけるとクラスメソッドになる def self.create_foods(names) names.map do |name| Food.new(name) end end #インスタンスメソッド def eat "I will eat an #{@name}." end end names = ['apple', 'octopus'] #クラスメソッドの呼び出し foods = Food.create_foods(names) foods.each do |food| puts food.eat end #=> I will eat an apple. # I will eat an octopus.参照
プロを目指すためのRuby入門
- 投稿日:2020-07-15T11:42:11+09:00
attr_accessorについて
インスタンス変数とは
同じインスタンスの内部で共有される変数
class Food def initialize(name) #インスタンス作成時に渡された名前をインスタンス変数に保存する @name = name end def eat "I will eat an #{@name}" end end food = Food.new('apple') food.hello #=> "I will eat an apple"インスタンス変数はクラスの外部から参照することができない!
class Food def initialize(name) @name = name end #@nameを外部から参照するためのメソッド def name @name end end food = Food.new('apple') #nameメソッドを経由して@nameの内容を取得 food.name #=> "apple"インスタンス変数の内容を外部から変更したい場合も変更用のメソッドを定義
class Food def initialize(name) @name = name end #@nameを外部から参照するためのメソッド def name @name end #@nameを外部から変更するためのメソッド def name=(value) @name = value end end food = Food.new('apple') #変数に代入しているように見えるが、実際はname=メソッドを呼び出している。 food.name = 'banana' #nameメソッドを経由して@nameの内容を取得 food.name #=> "banana"インスタンス変数の値を読み書きするメソッドのことを「アクセサメソッド」と呼ぶ。
これらのメソッドをいちいち書くのは面倒。
そこで登場するのが「attr_accessorメソッド」attr_accessorメソッド
class Food #@nameを読み書きするメソッドが自動的に定義される attr_accessor :name def initialize(name) @name = name end #@nameを外部から参照するためのメソッド #def name #@name #end #@nameを外部から変更するためのメソッド #def name=(value) #@name = value #end end food = Food.new('apple') #@nameを変更する food.name = 'banana' #@nameを参照する food.name #=> "banana"インスタンス変数の内容を読み取り専用にしたい場合は「attr_reader」
書き込み専用にしたい場合は「attr_writer」参照
プロを目指す人のためのRuby入門
- 投稿日:2020-07-15T11:02:34+09:00
initializeメソッド
- 投稿日:2020-07-15T10:23:04+09:00
オブジェクト指向のプログラミング用語
クラス
・データの型
・別名:「オブジェクトの設計図」「オブジェクトのひな形」
・クラスが同じであれば、保持している属性(データ項目)や使用できるメソッドが同じオブジェクト、インスタンス、レシーバ
・クラスをもとに作られたデータのかたまり
・メソッドとの関係を説明する場合、レシーバーと呼ばれることもある。food = Food.new('apple', '7:00') food.nameこのコードは以下のように説明される場合がある。
「2行目でFoodオブジェクトのnameメソッドを呼び出しています。」
「ここでのnameメソッドのレシーバはfoodです。」メソッド、メッセージ
・処理をひとまとめにし、名前をつけたもの。
food = Food.new('apple', '7:00') food.name「2行目ではfoodというレシーバーに対して、nameというメッセージを送っている」
状態(ステート)
・オブジェクトごとに保持されるデータのこと
・Foodクラスがもつ「名前」や「年齢」といったデータも「Foodの状態」に含まれる属性(アトリビュート、プロパティ)
・オブジェクトから取得できる値のこと
food = Food.new('apple', '7:00') food.name = 'apple'参照
プロを目指す人のためのRuby入門