20200715のRubyに関する記事は29件です。

【Rails】createメソッドで保存ができない時に使える「pry-rails」

経緯

ポートフォリオ作成中にモデルの保存がなかなかできずにハマってしまった。
「pry-rails」というgemを使うことで解決できたのでメモします。

コントローラ

以下のようなコード、メソッドを組んでました。

records_controller.rb
class 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.rb
class 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は、エラー原因特定にすごく便利なライブラリでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
スクリーンショット 2020-07-15 23.06.34.png

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:00

YAML形式の環境変数ファイル作成

$ cd workflow
$ mkdir config
$ vi environment.yml

development, staging, production別にurl, user,passwordを設定しました。
この環境変数をDigdagのタスクから参照します。

environment.yml
development:
  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_password

YAMLファイルの環境変数をDigdagの環境変数に設定するRubyクラスを作ります。

workflow フォルダの下にtasks フォルダを作ります
そこにenvironment.rbを作ります。こちらのクラスがYAMLファイルから環境変数を読み取ってDigdagの環境変数に保存します。

$ cd workflow
$ mkdir tasks
environment.rb
require '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
end

Workflow.digにタスク追加

$ digdag init workflow で自動生成されたコードは削除して以下のコードを追加

workflow.dig
timezone: 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 --rerun

development環境で実行結果
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 --rerun

staging環境で実行結果
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 --rerun

production環境で実行結果
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のチュートリアルアプリのデータでバッチを作ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

freeeAPI 請求書発行したら、自動で+更新までやっちゃおうぜ!!

こんにちは、未経験からエンジニア転職を目指しているmkです。
今回はfreeeAPIを触ってみました。

表題の通り、請求書をアップロードして+更新を毎回ぽちぽちやるのが面倒なので、
請求書をAPI経由で発行して、+更新まで全てやってくれるプログラムをRubyで書いてみました。

github

環境

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.rb
module 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.rb
require '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.rb
require_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.rb
require '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

ローカルで実行してみます。

スクリーンショット 2020-07-15 20.01.48.png

ちゃんと処理は走ったみたいです。実行時間は5秒程(ちょっと長い、、、)
会計freeeをみてみます。

スクリーンショット 2020-07-15 19.54.43.png

CFO株式会社宛に請求書が発行できてますね!
金額も消費税額もきちんと入ってます。
スクリーンショット 2020-07-15 22.06.04.png
次は取引を見てみましょう。

スクリーンショット 2020-07-15 19.53.04.png
ばっちし更新後の金額が0円になってます!

スクリーンショット 2020-07-15 19.52.21.png

更新日付も発行月の月末含む、12ヶ月分の末日が入っています。

今後の課題

月末発行の請求書はどうするんだとか、振り込み確認できた翌月から更新をかけたいだとか、柔軟に対応できるよう改良していきたいと思います。
また請求書の各パラメータと更新回数をcsvでインポートできるようします。
あとコードが美しくない、、、笑

日々精進

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【memberとcollection】

memberとcollectionとは

現在ツイッターの写真投稿版のようなアプリを作成しているのですが、その作成途中で投稿の検索機能を実装する際に出てきたmemberとcollectionについてとそれぞれの違いについてアウトプットします。

memberとcollectionはルーティングを設定する際に使用しすることができ、生成されるURLと実行されるコントローラを任意にカスタムすることができます。

collectionはルーティングに:idがつかない、memberはつくという違いがあります。

search1.png

上の画像のようにtweetsコントローラーのネストに入れてcollectionを記述すると、
以下の画像のルーティングになります。

search2.png

URIを見てもidが入っていません。

一方、memberで記述した場合

search3.png

この場合のルーティングは以下の画像のようになります。

search4.png

このようにルーティングにidが入っています。

今回作成した写真投稿アプリの場合は検索結果としてあくまで一覧表示させたいだけでidごとの詳細ページは必要ないのでcollectionを使用しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RaspberryPi3 + GPS受信キット +

Goal

  • GPSで位置情報が取れること

使ったもの

接続

手元に部品がそろっていたので秋月さまサイトに掲載されているマニュアルの回路図のまま組みました。スイッチングダイオードは実際は不要みたいですが、壊さないよう保険にもなるので繋ぎました。

image.png

http://akizukidenshi.com/download/ds/akizuki/AE-GPS_manual_r1.06_s.pdf

初期設定でGPSのアンテナが真上を向く必要があったので、ブレッドボードが縦になっています。
ちなみに初期動作テストでLEDが点滅するまで30分くらいかかりました。窓際に置いたのですが置いた場所が悪かったのかも。

IMG_4489.jpg

ソース

下記ソースを実行するにはラズパイでUART接続関連の設定をする必要があります。そこはググってください:pray:
それさえすめばあとは特に大したことはやっていません。

なお、NMEAフォーマット解析用にnmea_plusを使いました。 SourceDecoder まぢ便利:point_up:

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が表示されますよー。
姫島村のオフィスです:grinning:

image.png

感想

  • GPS受信キットのピンの並びがラズパイのピンに合わせてある(と思われる)ので繋ぐとき迷わなかった。
  • 探せばたいてい便利なGemが見つかりますねー。意地でもRubyを使おう。
  • あ、1PPS端子繋いだのに遊ぶの忘れてた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RaspberryPi3でGPS受信キットを使ってみる(Ruby)

Goal

  • GPSで位置情報が取れること

使ったもの

接続

手元に部品がそろっていたので秋月さまサイトに掲載されているマニュアルの回路図のまま組みました。スイッチングダイオードは実際は不要みたいですが、壊さないよう保険にもなるので繋ぎました。

image.png

http://akizukidenshi.com/download/ds/akizuki/AE-GPS_manual_r1.06_s.pdf

初期設定でGPSのアンテナが真上を向く必要があったので、ブレッドボードが縦になっています。
ちなみに初期動作テストでLEDが点滅するまで30分くらいかかりました。窓際に置いたのですが置いた場所が悪かったのかも。

IMG_4489.jpg

ソース

下記ソースを実行するにはラズパイでUART接続関連の設定をする必要があります。そこはググってください:pray:
それさえすめばあとは特に大したことはやっていません。

なお、NMEAフォーマット解析用にnmea_plusを使いました。 SourceDecoder まぢ便利:point_up:

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が表示されますよー。
姫島村のオフィスです:grinning:

image.png

感想

  • GPS受信キットのピンの並びがラズパイのピンに合わせてある(と思われる)ので繋ぐとき迷わなかった。
  • 探せばたいてい便利なGemが見つかりますねー。意地でもRubyを使おう。
  • あ、1PPS端子繋いだのに遊ぶの忘れてた。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒1年目がTDDを活かした実装方法を考えさせていただきました(ruby)

なぜ新卒1年目がTDDに注目したのか

現在新卒1年目でエンジニアとして働いている。エンジニアリングの経験が浅い自分が、経験の浅さを活かして先輩に何かで勝てないか考えていた。

経験が浅いので実装手順に癖がない。癖がないからこそ、いい癖をつけておこうと考えて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.rb
class 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.rb
def 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.rb
class 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を書いて、テスト実施されているか確認するとよい。
controller
  def 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6'

backend/Gemfile.lock

bundle installした際に依存関係を元にライブラリの設計図のようなものを作ってくれます。
空のものを作成しておきます。ファイルが存在しないと後でエラーになりました。

backend/Gemfile.lock

db/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.yml
version: "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.yml
default: &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:create

dockerの起動

ここまで来れば設定は完了です。
あとはdockerを起動するのみです!

% docker-compose up

ブラウザでRailsが立ち上がっていること確認

ブラウザで http://localhost:3000/ へアクセスすると、無事Railsが立ち上がっています。
スクリーンショット 2020-07-15 21.24.05.png

拡張性

今回は言及しませんでしたが、frontendコンテナなどを追加することによってフロントエンドアプリケーションとAPIサーバを兼ね備えた環境を一発で作ることができるようになります。
その際は、同様の手順でDockerfileの作成やdocker-composeへのコンテナの追加などを行っていくことになりまね。
僕はまだフロントエンド初心者なので、ゆくゆくはフロントのコンテナも作っていきたいです。

感想

やっぱりdockerは便利ですね。
自分個人の開発ではdockerは使っていませんでしたが、これからは積極的に使っていこうかなと思っています。
またその際には記事にしてあげていければと思います。

qiitaの記事を書くのが初めてで、どれくらいのものを書けばいいのか悩みながら書いていたら、だいぶ長くなってしまいました。
ただ、とても内容が濃い記事を何本も書いている人がたくさんいて、自分はまだまだだなぁと日々感じています。
僕はまだ内容があるものは頻繁には書けないかもしれませんが、少しずづ知識を記事化していければと思います。

参考にしたもの

以下のqiitaの記事を参考にさせていただきました。
書き方も真似させていただき、かなり参考になりました。
ありがとうございます。

https://qiita.com/azul915/items/5b7063cbc80192343fc0
https://qiita.com/kodai_0122/items/795438d738386c2c1966

上記の記事に加えて、APIモードやMySQLコンテナのマウントや設定なども盛り込んだ内容となっているので、書き方が似ている点はご容赦いただければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails 退会】railsで簡易的な退会機能を作る

【Rails 退会】railsで退会機能を作る

前提条件

モデル:Userモデル
コントローラー:Usersコントローラー
ルーティング:resources :usersを記述

機能をつける前の実装プロセス設計

①投稿機能をつける場合と同様に、Userのidをキーとしてレコードを取得
②そのレコードをdestroyメソッドを用いて、削除する

実際にやってきましょう!

Users_controller.rbをいじいじ

users_controller
class UsersController < ApplicationController
  def destroy
    @user = User.find(params[:id]) #特定のidを持つ情報を取得
    @user.destroy
    flash[:success] = 'ユーザーを削除しました。'
    redirect_to :root #削除に成功すればrootページに戻る
  end
end

viewをいじいじ

〇〇.html.erb
<%= link_to "退会する",user_path(current_user.id), class: "destroy-user",method: :delete %>

<%# ここで、(current_user.id)でしっかり引数にログインユーザーのidを渡してあげること!

これだけで完了!
高度な退会機能はまだ作れなかった...
とりあえず!!一旦これで!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者がはまりがちな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に英語で記事を書いたので、そちらもご覧になってみてください。

https://medium.com/@shogohida_81081/5-solutions-to-common-errors-of-docker-for-beginners-c04dc1237c78

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

そうだ、画像をプレビューしよう。~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と同時に一つずつ番号を割り振るメソッドです。

さて、これでプレビュー機能も実装完了となります。
ということは、

最後に

遂に完成です!!結構な時間がかかりましたが、なんとか終えることができました。ふぅ。

完成とは言いつつもできたのは機能面だけなので、あとはゆっくりマークアップをしていく感じになります。

とはいえ仕様書の内容は全て実装することができました。実際のところ大して難しいことをしてるわけでもないのに長々と書いてしまってすみません。

いないとは思いますが、最後まで読んだ方がいらっしゃいましたら長らくどうもありがとうございました。何かの参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 doctor

The 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.dylib

mysqlが依存している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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

未経験初学者が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の内容や自分と一緒の場所でつまずいている方が仮にこの記事を参考にしてくれた場合の動線の意識など反省点がや改善点が多々ありました。
今後もこのようなアウトプットを続けて行こうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

高速なのは 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

と書いても似たような結果を得られました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【form_withのmodel:の意味】

form_withに記述するmodel:の意味

いまいち理解できていなかったのですがようやく意味がわかったのでアウトプットとして記述していきます。

model-.png

上の画像はツイートの詳細画面からコメントを投稿するためのフォームを表示させるためのコードを記述しているのですが、
form_withに続くmodel:@tweet, @commentの二つをなぜ使用しているのかいまいち理解できていませんでしたが解決できましたので以下に理由を記述します。

1点目

今回の場合はツイートに対してコメントをするという状況でありコメントは一つのツイートに紐付いている状態なのでコメント単体でフォームの送信先に指定することはできないため@tweet, @commentと紐付いているものとセットにして送信先を指定しているという理由。

2点目

@tweet,@commentという記述自体がルーティングを指定しているということ。
どういうことかといいますと、以下の画像を見てください。

model2.png

今回の場合はコメントを作成したいので発動させたいアクションはcommentsコントローラーのcreateアクションです。
そして一番左のprefixの項目を見ると、tweet_commentsというパスが記述されています。
要するにフォームを送信してコメントを作成するのであればこのルーティングを指定しなければいけないということです。

そのルーティングを指定しているのがmodel: [@tweet,@comment]だというのが二点目の理由です。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unicornを再起動する

前提

RailsアプリをEC2にデプロイし、Unicornをアプリケーションサーバーとして使用しています。

開発環境でしたら、rails sで起動すれば良かったのですが、本番環境はサーバーの起動&停止方法が変わったので、メモとしてまとめました。

サーバーを停止したい場合の手順

Unicornの起動確認

ps -ef | grep unicorn | grep -v grep
vinaka     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)15533killします(毎回番号が変わるので都度ps -ef | grep unicorn | grep -v grepで番号を確認)

kill -9 15533

Unicornの停止確認

ps -ef | grep unicorn | grep -v grep

すると何も表示されないはず。
表示されていないと、Unicornが停止しています。

再度サーバーを立ち上げる時の手順

Unicornの停止確認

ps -ef | grep unicorn | grep -v grep

Unicorn起動!

 bundle exec unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E production 

何も出ないと、ちゃんと起動しているようです。

一応確認もする。

ps -ef | grep unicorn | grep -v grep
vinaka     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でバージョン指定するのが良いです。)

Gemfile
group :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 production

nginxも再起動

Unicornが無事に立ち上がったら、nginxも再起動してあげて完了です。

sudo nginx -s reload

終わりです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

参考にした記事

rbenvの役割 - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Progate Ruby on Rails5 振り返り

プロゲート Ruby on Railsコース所感

プログラミングの学習に200時間ほど費やし、Railsコースに着手しました。
今までの学習ではスムーズに進んでこれたものの、今回は苦戦。
道場コース4の内容は完全には理解できていません。
復習できるように、メモとして記録を残します。


マイグレーションファイル関連

マイグレーションファイルとは:データベースを作成するときの設計図。
モデルとは:テーブルを操作するための特殊なクラス
  
モデルの作成

rails g model Post content:text 

rails g model モデル名 カラム名:データ型
※モデル名は単数系で指定する。
  
マイグレーションファイルの作成

rails g migration add_image_name

URLから値を取得する

ルーティングでハッシュを指定する。

routes.rb
get "posts/:id" => "posts#show"

params[ハッシュ]とすることで取得できる

controller.rb
def 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.rb
before_action :set_current_user

def set_current_user
  @current_user = User.find_by(id: sessino[:user_id])
end

投稿テーブルに持たせたuser_idから別テーブルに存在するユーザー情報を取得する

使用するデータが存在するテーブル(ここではPostモデル)に対してアクションを定義する

post.rb
def user
  return User.find_by(id: self.user_id)
end

活用例(投稿から、それに紐づくユーザーを取得する)

posts_controller.rb
def show
  @post = Post.find_by(id: params[:id])
  @user = @post.user
end

ユーザーテーブルに持たせたidから別テーブルに存在する投稿情報を取得する

使用するデータが存在するテーブル(ここではUserモデル)に対してアクションを定義する

user.rb
def 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.rb
get "users/:id/likes" => "users#likes"

アクションの定義

users.controller.rb
def 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を用いて、対応する投稿を取得する部分を忘れないように。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「商品画像を差し替える」と心の中で思ったならッ!その時スデに行動は終わっているんだッ!~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で!!

商品情報編集機能を実装したい~part5~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

分かりやすいプログラムを書くために意識する事8選!!

・ "分かりやすいプログラム"とはどんなの?
・ 何に気を付けて書けば"分かりやすいプログラム"になるのだろう?

プログラミングを進めていくと"分かりやすいプログラムが重要だよ!"と言われることがあると思いますが、最初はいまいちピンとこないですよね。
今回は「勉強会に参加して学んだ事」や「オリジナルアプリをレビューしてもらった際に学んだこと」を元に、自身も日々意識すべき内容として分かりやすいプログラムについて気を付けるPoint8選としてまとめていこうと思います。


分かりやすいプログラムとは

結論: コードを読む人を意識する

数ヶ月前の自分のコードを見た時に「読みにくいな..誰がこんなコード書いたんだ?!」とならないようにしたいですね。
そして、チーム開発する場合においても読みにくいコードだとチームのメンバーにも迷惑をかけたり、開発する上で誤解が生じやすくなります。
常に読み手を意識してコードを書く習慣をつけておくのは大切だと思います。

ここからは、具体的に何を学んで、何を意識するべきか書き出していこうと思います。
(基本的な内容もありますが過去の自分で感じた事は、他の方の参考にもなると思って載せます)

Point1: ファイルの分割

main.rb
require "./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.rb
require "./messages"
require "./controller"

class Game
  def initialize(style, count, data)

  end

  def question_style
    # 各問題に対する処理
  end

end

前提条件:game.question_styleを行うと、クイズの問題定義の形式を振り分けた上でゲームの処理が開始する状況の場合です。
注目箇所:

ゲームプログラムにおいてquestion_styleというメソッド名を見た時、皆さんは何の処理をしているメソッドだと思いますか?

質問の様式を決めている?..質問の様式を選択してる?..
少なくともメソッド名を見ただけでは、"各問題に対する処理が入っている"とはわからないですよね。

つまり、
中のコードを読んだ時に初めて何の処理を行うメソッドなのかがわかるメソッド名="分かりにくメソッド名"になってしまいます。

メソッド名がquestion_styleだと、コードを書いた側目線!
"問題定義の形式を決めて回答処理を切り替えるようにした"という実装の都合になってしまいます。

今回の例でいうと、メソッド名はstartplayなどを使うとどうでしょう?
ゲームプログラムにおいて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

あとがき

少しでも参考になれば嬉しいです!!

プログラミングは苦しいこともたくさんありますが..汗
やっていて本当に楽しいですね!
楽しんで頑張って継続しましょうね!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ターミナルで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
=> Run rails 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!が表示されました。

詰まったときには再起動すれば解決することが多いと思うので試してみてください。(適当)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Capybaraを使った統合テスト(導入〜簡単なテスト実行まで)

はじめに

統合テストとはアプリ上で意図したとおりの動作をしてくれるかを確認するテストのことです。
ブラウザ上で自分で行なっても良いですが、テストコードを書くことで大きなメリットがあるのでできれば書いておくと良いかと思います。

メリット
・一度コードを書いてしまえばテストが簡単にできる
(テストコードがないとフォームの入力やリンクの移動など毎回行なう必要がある)
・思考を整理できる
 書き下ろすことで自分の思考を客観視でき、抜け漏れに気付きやすくなる
・記録として残せる
 アプリの動作を自分がどこまで想定しているか見返すことができる
 また、第3者がテスト内容を用意に確認できる

今回は以下のGIFファイルのように、投稿する動作がうまく行くかどうか確認してみようと思います。
e02d69df1a47ecac2b896cfeb09626d2.gif

Capybara導入

まずはCapybaraというgemを導入します。

Gemfile
group :test, :development do
  # (省略)
  gem 'capybara'
end

※ このときtest環境の中にcapybaraが既に入っている可能性があるので、そのときはコメントアウトしておきましょう。rails5.2.3では入ってました。

ターミナル
$ bundle install
spec/rails_helper.rb
# 以下の記述を追加
require 'capybara/rspec'

次に作業ファイルを用意します。
spec/featuresディレクトリを作成しましょう。
その中にテスト用のファイルを作成します。
今回はpost_spec.rbとします。

テストコード

post_spec.rbに記述していきます。

spec/features/post_spec.rb
require '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.rb

spec以下で実行したいファイルを指定します。
今回はfeaturesディレクトリのpost_spec.rbにテストコードを書いたので上のようになります。

では実行してみます。

12172459f80a1f0c542410796c6829fe.png
上のように表示されれば無事クリアしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【N+1問題】

N+1問題とは

アソシエーションでモデル同士を関連づけている場合に起こる問題のこと。

例えばツイッターのようなアプリでユーザーと投稿が紐付いている場合に
トップページで投稿を一覧表示させる時に各投稿ごとに紐付いているユーザーを検索するために毎回毎回データベースにアクセスしているとアプリのパフォーマンスが下がってしまう。

1万投稿ある場合は1万回データベースにアクセスしなければいけないということ。

この問題のことをN+1問題という。

この問題を解決するためのメソッドが

includeメソッド

と言われるメソッド。

このメソッドは毎回毎回データベースにアクセスして紐づくデータを検索していたところを、
一回で全データを取得してくれる。

そのため1万投稿あるとしてもデータベースへのアクセス「は一度で済む。

n+1.png

上の画像のようにindexアクションの処理内容の箇所にincludes(:紐づくモデル)とすれば使用できる。

これでツイートなどを一覧表示する際に投稿がいくら増えたとしてもパフォーマンスを下げずに済む。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.rb
User.order(id: desc).limit(5)

1.引数なし

models/user.rb
class User < ApplicationRecord
  scope :recent, -> { order(id: :desc).limit(5) }
end
users_controller.rb
User.recent

2.引数あり

models/user.rb
class User < ApplicationRecord
  scope :recent, -> (count) { order(id: :desc).limit(count) }
end
users_controller.rb
User.recent(5)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsアプリをawsでインフラ構築する際に出たエラー

今回はこちらの記事を参考に、始めて作成したrailsのappをAWSを使用してインフラ構築に挑戦した。
https://qiita.com/naoki_mochizuki/items/814e0979217b1a25aa3e

EC2インスタンスの環境構築

$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%A5

Nginxの起動

以下のコマンドを入力したが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/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラスメソッド

クラスメソッドとは

クラスに関連が深いものの、一つ一つのインスタンスに含まれるデータを使わないメソッド

クラスメソッドを定義する方法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入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

initializeメソッド

initializeメソッド

・クラスからオブジェクトを作成する場合に呼び出されるメソッド

class Food
  def initialize
    puts 'Initialize'
  end
end
Food.new
#=> Initialized.

・initializeメソッドは特殊なメソッドで外部から呼び出すことはできない

food = Food.new
food.initialize
#=> NoMethodError: private method 'initialize' call for #<Food:0x007gfasjfasfjas>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクト指向のプログラミング用語

クラス

・データの型
・別名:「オブジェクトの設計図」「オブジェクトのひな形」
・クラスが同じであれば、保持している属性(データ項目)や使用できるメソッドが同じ

オブジェクト、インスタンス、レシーバ

・クラスをもとに作られたデータのかたまり
・メソッドとの関係を説明する場合、レシーバーと呼ばれることもある。

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入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む