20200315のRubyに関する記事は28件です。

マイクロポストにタグ付けする

はじめに

ご訪問いただきありがとうございます。レベル的にはrailsチュートリアルを終えたくらいのレベルです。もし間違えているところ等あればアドバイスいただけると幸いです。

作るもの

今回はマイクロポストに自由にタグ付けと編集の機能を追加します。
(題材は自分のポートフォリオサイトです。尚、すでにUserテーブルとMicropostテーブルは作成済みです。)

対象読者

railsチュートリアルに機能を追加したい等自分と同じ位のレベルの人を対象としています。

作成の流れ

1.TagモデルとTag_relationshipモデルの作成
2.モデルの関連付けとバリテーション
3.対応するビューの作成
4.コントローラーのアクション作成
5.タグ編集機能の作成

1.TagモデルとTag_relationshipモデルの作成

今回はタグ付機能を作ります。マイクロポストから見ると一つのポストに一つ以上のタグをつけることができる。タグから見ると一つのタグは一つ以上のマイクロポストに関連づくということになります。つまり多対多の関連付けとなり、MicropostもでるとTagモデルの中間にTagrelationテーブルを設ける方法で今回は進めます。それぞれの中身はこんな感じです。
Tag_relationship

カラム
id integer
micropost_id integer
tag_id integer

Tag

カラム
id integer
name string

モデルを作成します。

rails g model tag name:string
rails g model tag_relationship micropost:references tag:references 

作成されたマイグレーションファイルを確認します。

db/migrate
class CreateTags < ActiveRecord::Migration[5.1]
  def change
    create_table :tags do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end

nameを空で保存されるのを防ぐためにnull:falseを追記してます。

db/migrate
class CreateTagRelationships < class CreateTagRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :tag_relationships do |t|
      t.references :micropost, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
      add_index :tag_relationships, [:micropost_id,:tag_id],unique: true
  end
end


こちらはreferencesを付けて作成したので自動的にreferencesが設定されています。また、後々nameで検索する可能性を踏まえindexをnameに追記しました。unipue:tureは同じ名前のタグを保存できないようにしています。
ちなみにadd_index :テーブル名, :カラム名で指定します。
※referencesはbelongs_toのエイリアスです。(tag_idは一つのTag.idと紐づく)
migrateを行います。

rails db:migrate

2モデルの関連付けとバリレーション

各モデルの関連付けは以下のようになります。

app/model/tag.rb
class Tag < ApplicationRecord
  has_many   :tag_relationships, dependent: :destroy
  has_many   :microposts, through: :tag_relationships
  validates :name, uniqueness: true
end
ruby/app/model/tag_relationship.rb
class TagRelationship < ApplicationRecord
  belongs_to :micropost
  belongs_to :tag
end
app/model/micropost.rb
has_many  :tag_relationships, dependent: :destroy
  has_many  :tags, through: :tag_relationships

ここで「has_many:microposts, through: :tag_relationships」
とすることによって、tag_relationshipモデルを通してタグに紐づくマイクロポストを取得することができるようになります。(マイクロポスト側も同じ)
注意点として、スルーの関連付けの前にrelationshipsの中間テーブルの関連付けを記載しないとうまく動作しません。(先にhas_many:tags~とすると中間テーブルが関連づいてないので中間テーブルがありませんとエラーが出ます。)
validates :name, uniqueness: trueでタグの名前が重複して保存されるのを防ぎます。

3対応するビューの作成

今回はマイクロポストを作成する際にタグをつけるような設定とします。

app/views/microposts/new
<%= form_with model:@micropost, local: true do |f| %>
※タグの部分のみ抜粋
<%= f.text_field :tag_ids, class: "form-control", id:'tag_ids',\
    placeholder: "タグをつける。複数つけるには','で区切ってください。" %>

<% end %>

タグを複数つけるときは(,)で区切ってもらうようにします。
「f.text_field :tag_ids」と記載することでマイクロポストを作成するタイミングでタグの情報も一緒に送ります。
※tag_idsはマイクロポストで「has_many :tags, through: :tag_relationships」の関連付けをすることよってマイクロポストオブジェクトに使用できるようになります。

例
Micropost.first.tag_ids
=> [2, 3, 4, 5]

タグはマイクロポストの中に表示するので、マイクロポストのビューの中にタグの表示を追記します。

app/views/microposts/_micropost.html.erb
※タグを表示する部分のみ抜粋
<% micropost.tags.each do |tag| %>
   <%= tag.name %>
<% end %>

4コントローラのアクション作成

マイクロポストの情報と一緒にタグの情報も送られてくるようになったのでマイクロポストのcreateアクションでタグも保存されるようにします。

app/controllers/microposts_controller.rb
def create
    @micropost = current_user.microposts.build(micropost_params)
    tag_list = params[:micropost][:tag_ids].split(',')
    if @micropost.save
      @micropost.save_tags(tag_list)
      flash[:success] = '投稿しました!'
      redirect_to root_url
    else
      render 'new'
    end
  end

「tag_list」で送られてくるタグの値を取得します。複数のタグ付を行うときにsplit(',')で区切るようにします。
「@micropost.save_tags(tag_list)」でマイクロポストにタグを関連付けます。save_tagsメゾットの中身はこんな感じです。

app/model/micropost.rb
def save_tags(savemicropost_tags)
    savemicropost_tags.each do |new_name|
    micropost_tag = Tag.find_or_create_by(name: new_name)
    self.tags << micropost_tag
end

「find_or_create_by」メゾットはカラムの中から同じ値がないか探して、あればそのままfindの動き、なければcreateの動きで新たにカラムに保存します。
これでタグ付機能は作成完了です。

タグ編集機能の作成

ここからはタグの編集機能を付けていきます。
タグの編集はマイクロポストの編集ページで行います。(すでにマイクロポスト編集ページ作成済みの前提で行います)

※タグの部分のみ抜粋(form_withを使用しているのでrenderでマイクロポストのフォームページと共有できます。)

app/micropost/edit
<%= f.text_field :tag_ids,value: @tag_list,\
    placeholder: "タグをつける。複数つけるには','で区切ってください。" %>

ここで「@tag_list」で既存の値を表示しています。
それを踏まえたコントローラのアクションがこちらです。

app/controllers/microposts_controller.rb
def edit
    @micropost =Micropost.find(params[:id])
    @tag_list =@micropost.tags.pluck(:name).join(",")
  end

  def update
    @micropost =Micropost.find(params[:id])
    tag_list = params[:micropost][:tag_ids].split(',')
    if @micropost.update_attributes(micropost_params)
      @micropost.save_tags(tag_list)
      flash[:success] = '投稿を編集しました‼'
      redirect_to @micropost
    else
    render 'edit'
    end
  end

まずeditアクションには既存のタグを取得すために「@tag_list」を追記します。
tagsでマイクロポストに関連するタグを取得
pluck(:name)でタグのnameの配列を取得
join(",")で配列を,で区切った文字列として取得します。

「save_tags」を更新用に編集します。

※マイクロポスト編集ページへの「link_to」で行き先を「edit_micropost_path」としたときにマイクロポストshowページ以外からリンクを踏んでアクセスするとタグフォームに既存の値が入らずうまくタグの更新ができなかったので「/microposts/#{micropost.id}/editでリンクを設定してます。

app/model/micropost.rb
def save_tags(savemicropost_tags)
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    old_tags = current_tags - savemicropost_tags
    new_tags = savemicropost_tags - current_tags

    old_tags.each do |old_name|
      self.tags.delete Tag.find_by(name: old_name)
    end

    new_tags.each do |new_name|
      micropost_tag = Tag.find_or_create_by(name: new_name)
      self.tags << micropost_tag
    end
  end

動作は既存のタグを残すものと消すものに分けて、新しいタグを既存の残すタグに追加していくといった形です。
「current_tags」既存のタグを取得
「old_tags」消すタグを取得。
「new_tags」新たに追加するタグを取得。
old_tagsとnew_tagsにある「ー」は引き算です。
それぞれ引き算して残ったタグを繰り返し処理して完了です。

current_tags - savemicropost_tags =old_tags
「test, test2, test3」 - [test, test3] =[test2]

以上でタグ付機能は完成となります。最後までお読みいただきありがとうございました。
アドバイス等いただけるととても喜びます。次は今回作ったタグを利用した検索機能を作ろうと思います。
ありがとうございました。

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

railsタグ付機能(rails チュートリアル後の機能追加)

はじめに

ご訪問いただきありがとうございます。レベル的にはrailsチュートリアルを終えたくらいのレベルです。もし間違えているところ等あればアドバイスいただけると幸いです。

作るもの

今回はマイクロポストに自由にタグ付けと編集の機能を追加します。
(題材は自分のポートフォリオサイトです。尚、すでにUserテーブルとMicropostテーブルは作成済みです。)

対象読者

railsチュートリアルに機能を追加したい等自分と同じ位のレベルの人を対象としています。

作成の流れ

1.TagモデルとTag_relationshipモデルの作成
2.モデルの関連付けとバリテーション
3.対応するビューの作成
4.コントローラーのアクション作成
5.タグ編集機能の作成

1.TagモデルとTag_relationshipモデルの作成

今回はタグ付機能を作ります。マイクロポストから見ると一つのポストに一つ以上のタグをつけることができる。タグから見ると一つのタグは一つ以上のマイクロポストに関連づくということになります。つまり多対多の関連付けとなり、MicropostもでるとTagモデルの中間にTagrelationテーブルを設ける方法で今回は進めます。それぞれの中身はこんな感じです。
Tag_relationship

カラム
id integer
micropost_id integer
tag_id integer

Tag

カラム
id integer
name string

モデルを作成します。

rails g model tag name:string
rails g model tag_relationship micropost:references tag:references 

作成されたマイグレーションファイルを確認します。

db/migrate
class CreateTags < ActiveRecord::Migration[5.1]
  def change
    create_table :tags do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end

nameを空で保存されるのを防ぐためにnull:falseを追記してます。

db/migrate
class CreateTagRelationships < class CreateTagRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :tag_relationships do |t|
      t.references :micropost, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
      add_index :tag_relationships, [:micropost_id,:tag_id],unique: true
  end
end


こちらはreferencesを付けて作成したので自動的にreferencesが設定されています。また、後々nameで検索する可能性を踏まえindexをnameに追記しました。unipue:tureは同じ名前のタグを保存できないようにしています。
ちなみにadd_index :テーブル名, :カラム名で指定します。
※referencesはbelongs_toのエイリアスです。(tag_idは一つのTag.idと紐づく)
migrateを行います。

rails db:migrate

2モデルの関連付けとバリレーション

各モデルの関連付けは以下のようになります。

app/model/tag.rb
class Tag < ApplicationRecord
  has_many   :tag_relationships, dependent: :destroy
  has_many   :microposts, through: :tag_relationships
  validates :name, uniqueness: true
end
ruby/app/model/tag_relationship.rb
class TagRelationship < ApplicationRecord
  belongs_to :micropost
  belongs_to :tag
end
app/model/micropost.rb
has_many  :tag_relationships, dependent: :destroy
  has_many  :tags, through: :tag_relationships

ここで「has_many:microposts, through: :tag_relationships」
とすることによって、tag_relationshipモデルを通してタグに紐づくマイクロポストを取得することができるようになります。(マイクロポスト側も同じ)
注意点として、スルーの関連付けの前にrelationshipsの中間テーブルの関連付けを記載しないとうまく動作しません。(先にhas_many:tags~とすると中間テーブルが関連づいてないので中間テーブルがありませんとエラーが出ます。)
validates :name, uniqueness: trueでタグの名前が重複して保存されるのを防ぎます。

3対応するビューの作成

今回はマイクロポストを作成する際にタグをつけるような設定とします。

app/views/microposts/new
<%= form_with model:@micropost, local: true do |f| %>
※タグの部分のみ抜粋
<%= f.text_field :tag_ids, class: "form-control", id:'tag_ids',\
    placeholder: "タグをつける。複数つけるには','で区切ってください。" %>

<% end %>

タグを複数つけるときは(,)で区切ってもらうようにします。
「f.text_field :tag_ids」と記載することでマイクロポストを作成するタイミングでタグの情報も一緒に送ります。
※tag_idsはマイクロポストで「has_many :tags, through: :tag_relationships」の関連付けをすることよってマイクロポストオブジェクトに使用できるようになります。

例
Micropost.first.tag_ids
=> [2, 3, 4, 5]

タグはマイクロポストの中に表示するので、マイクロポストのビューの中にタグの表示を追記します。

app/views/microposts/_micropost.html.erb
※タグを表示する部分のみ抜粋
<% micropost.tags.each do |tag| %>
   <%= tag.name %>
<% end %>

4コントローラのアクション作成

マイクロポストの情報と一緒にタグの情報も送られてくるようになったのでマイクロポストのcreateアクションでタグも保存されるようにします。

app/controllers/microposts_controller.rb
def create
    @micropost = current_user.microposts.build(micropost_params)
    tag_list = params[:micropost][:tag_ids].split(',')
    if @micropost.save
      @micropost.save_tags(tag_list)
      flash[:success] = '投稿しました!'
      redirect_to root_url
    else
      render 'new'
    end
  end

「tag_list」で送られてくるタグの値を取得します。複数のタグ付を行うときにsplit(',')で区切るようにします。
「@micropost.save_tags(tag_list)」でマイクロポストにタグを関連付けます。save_tagsメゾットの中身はこんな感じです。

app/model/micropost.rb
def save_tags(savemicropost_tags)
    savemicropost_tags.each do |new_name|
    micropost_tag = Tag.find_or_create_by(name: new_name)
    self.tags << micropost_tag
end

「find_or_create_by」メゾットはカラムの中から同じ値がないか探して、あればそのままfindの動き、なければcreateの動きで新たにカラムに保存します。
これでタグ付機能は作成完了です。

タグ編集機能の作成

ここからはタグの編集機能を付けていきます。
タグの編集はマイクロポストの編集ページで行います。(すでにマイクロポスト編集ページ作成済みの前提で行います)

※タグの部分のみ抜粋(form_withを使用しているのでrenderでマイクロポストのフォームページと共有できます。)

app/micropost/edit
<%= f.text_field :tag_ids,value: @tag_list,\
    placeholder: "タグをつける。複数つけるには','で区切ってください。" %>

ここで「@tag_list」で既存の値を表示しています。
それを踏まえたコントローラのアクションがこちらです。

app/controllers/microposts_controller.rb
def edit
    @micropost =Micropost.find(params[:id])
    @tag_list =@micropost.tags.pluck(:name).join(",")
  end

  def update
    @micropost =Micropost.find(params[:id])
    tag_list = params[:micropost][:tag_ids].split(',')
    if @micropost.update_attributes(micropost_params)
      @micropost.save_tags(tag_list)
      flash[:success] = '投稿を編集しました‼'
      redirect_to @micropost
    else
    render 'edit'
    end
  end

まずeditアクションには既存のタグを取得すために「@tag_list」を追記します。
tagsでマイクロポストに関連するタグを取得
pluck(:name)でタグのnameの配列を取得
join(",")で配列を,で区切った文字列として取得します。

「save_tags」を更新用に編集します。

※マイクロポスト編集ページへの「link_to」で行き先を「edit_micropost_path」としたときにマイクロポストshowページ以外からリンクを踏んでアクセスするとタグフォームに既存の値が入らずうまくタグの更新ができなかったので「/microposts/#{micropost.id}/editでリンクを設定してます。

app/model/micropost.rb
def save_tags(savemicropost_tags)
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    old_tags = current_tags - savemicropost_tags
    new_tags = savemicropost_tags - current_tags

    old_tags.each do |old_name|
      self.tags.delete Tag.find_by(name: old_name)
    end

    new_tags.each do |new_name|
      micropost_tag = Tag.find_or_create_by(name: new_name)
      self.tags << micropost_tag
    end
  end

動作は既存のタグを残すものと消すものに分けて、新しいタグを既存の残すタグに追加していくといった形です。
「current_tags」既存のタグを取得
「old_tags」消すタグを取得。
「new_tags」新たに追加するタグを取得。
old_tagsとnew_tagsにある「ー」は引き算です。
それぞれ引き算して残ったタグを繰り返し処理して完了です。

current_tags - savemicropost_tags =old_tags
「test, test2, test3」 - [test, test3] =[test2]

以上でタグ付機能は完成となります。最後までお読みいただきありがとうございました。
アドバイス等いただけるととても喜びます。次は今回作ったタグを利用した検索機能を作ろうと思います。
ありがとうございました。

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

【Rails6・楽天API】商品検索機能の実装手順

はじめに

Railsで楽天APIを使用して商品(本)の検索、商品情報の取得・表示をする方法をまとめました。
今回は、本を題名で検索し、ヒットした本の情報を一覧にして下に表示する形です。

バージョン

・Ruby 2.7.0
・Rails 6.0.2.1

実装手順

アプリケーションIDの取得

楽天API情報を使用するには、Rakuten Developersより
「新規アプリ登録」を行いアプリケーションIDを取得する必要があります。
簡単な情報を入力してすぐに取得することができます。
AmazonAPIは申請を行い審査を受ける(落ちることも多いようです)ので、この点が違いますね。

gemをインストール

Ruby SDK(公式)に従って進めます。
Gemfileに下記追記して、bundleでインストール。

Gemfile
gem 'rakuten_web_service'

アプリケーションIDを設定

取得したアプリケーションIDを設定します。
config/initializersフォルダの中に下記ファイルを作成。

rakuten.rb
RakutenWebService.configure do |c|
  # (必須) アプリケーションID
  c.application_id = '*******************'

  # (任意) 楽天アフィリエイトID
  c.affiliate_id = '*******************'

end

こちらも、Ruby SDK(公式)を参照してください。
タイミングによっては書き方が微妙に異なるようです。

ビューの作成

search.html.slim
.search-box
  = form_tag(books_search_path, method: :get)
    .form-group
      = text_field_tag :keyword, '', id: "book_search", class: "form-control", 
                        name: "keyword", placeholder: "題名を入力してください" 
      button title='検索' class="form-control" type='submit'
        | 本の題名を検索
- if @books.present?
  = render 'books/book'
_book.html.slim
- @books.each do |book|
  = book.title
  = book.author
  = book.item_caption
  = link_to (image_tag(book.medium_image_url)), book.item_url

※フォーマットはかなり省いているので、適宜調整してください。

コントローラーの作成

books_controller.rb
class BooksController < ApplicationController
  def search
    if params[:keyword]
      @books = RakutenWebService::Books::Book.search(title: params[:keyword])
    end
  end
end

ルーティングの設定

routes.rb
get 'books/search'

題名検索→商品情報表示の流れ

上記コードで最低限の設定はOKのはずです。
本の題名を入力してから商品情報が表示されるまでの流れは下記のようになります。

text_field_tagに本の題名を入力して検索ボタンをおす
= form_tag(books_search_path, method: :get)により、
booksコントローラーのsearchアクションにいきます。
その際、入力した内容はparams[:keyword]に格納されています。

②楽天APIを使用した検索が行われる
入力した値params[:keyword]を引数としてsearchメソッドで検索をかけ、
ヒットしたものを@booksに格納します。
今回は本の検索で絞りたかったため、RakutenWebService::Books::Bookとしてます。

③再度ビューに展開する
検索結果が@booksに格納されたことにより、
ビューの- if @books.present?trueとなり、パーシャルが表示されます。
パーシャルでは、上から本の「題名」「著者」「説明文」「画像(商品リンク)」を表示しております。
使用できるプロパティはまだまだたくさんあり、楽天ブックス書籍検索API に一覧があります。

まとめ

検索結果から商品情報をビューの上でひっぱりだすところで苦戦しましたが、
公式に記載のパラメーターを落ち着いて確認することで対応ができました。
ご参考になりましたら幸いです。

参考

【Rails基礎】楽天APIで商品検索アプリを作ってみた
Ruby on Rails で楽天商品ページを取得してみた

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

ruby-dnnとディープラーニングでリンゴをオレンジに変換してみた(Cycle-GAN)

はじめに

今回はruby-dnnでCycle-GANを動かしてリンゴをオレンジに変換してみたいと思います。
RubyとCPUでCycle-GANという時点でもう無理ゲー感半端ないです?

コード全文はhttps://github.com/unagiootoro/apple2orange-cycleganにあります。

使用ライブラリ/バージョンなど

Ruby ... v2.6.5
ruby-dnn ... v1.1.4
Numo::NArray ... v0.9.1.5
Numo::Linalg ... v0.1.4
rubyzip ... v2.2.0

Cycle-GANざっくり解説

Cycle-GANとは、二つのドメインの異なる画像間でそれぞれ相互に変換ができるようにするモデルです。
画像の変換というと、前回やったPix2pixも画像変換ですが、Pix2pixは入力画像に対する出力画像が一対一でなければならないのに対して、Cycle-GANでは、一対一のペアがなくても変換元と変換先の画像さえあれば変換できるのが特徴です。

Cycle-GANでは、「DCGAN-A」「DCGAN-B」「Cycle Consistency Loss」の3つによって変換が行われます。イメージとしては、以下の図のような感じです。
(画像はペイントで作った手抜きです?)
img1.png

DCGAN-Aでは、リンゴがオレンジになることを学習させます。
逆にDCGAN-Bでは、オレンジがリンゴになることを学習させます。
しかし、これだけだと、リンゴの画像をオレンジに変換したとき、元のリンゴの画像の形状を保つことができないので、Cycle Consistency Lossが必要になります。

Cycle Consistency Loss

Cycle Consistency Lossは、DCGAN-Aでリンゴをオレンジに変換した画像にDCGAN-Bを適用すると元のリンゴに戻ることを学習させるために使用します。同じように、DCGAN-Bでオレンジをリンゴに変換した画像にDCGAN-Aを適用すると元のオレンジに戻ることも学習させます。

これによって、元のリンゴの画像の形状を保ったまま必要な箇所だけオレンジに変換することができるようになります。

ruby-dnnでCycle-GAN

モデル定義

Cycle-GANのモデル定義です。ほぼ前回やったPix2pixのモデルを使いまわしています。 1

各クラスの役割は、以下の通りです。

Generator: 入力画像から変換先の画像を生成します。

Discriminator: Generatorが生成した画像を受け取り、それが本物か生成された画像かを判断できるように学習させるためのモデルです。

DCGAN: Discriminatorを騙せるような画像を生成できるようにGenerator-Aを学習させるためのモデルです。また、Generator-Aが生成した画像にGenerator-Bを適用したとき、元の画像に戻るように学習させます。

長いので折りたたんでいます
class Generator < Model
  def initialize(input_shape, base_num_filters)
    super()
    @input_shape = input_shape
    @cv1 = Conv2D.new(base_num_filters, 4, padding: true)
    @cv2 = Conv2D.new(base_num_filters, 4, strides: 2, padding: true)
    @cv3 = Conv2D.new(base_num_filters * 2, 4, padding: true)
    @cv4 = Conv2D.new(base_num_filters * 2, 4, strides: 2, padding: true)
    @cv5 = Conv2D.new(base_num_filters * 2, 4, padding: true)
    @cv6 = Conv2D.new(base_num_filters, 4, padding: true)
    @cv7 = Conv2D.new(base_num_filters, 4, padding: true)
    @cv8 = Conv2D.new(3, 4, padding: true)
    @cvt1 = Conv2DTranspose.new(base_num_filters * 2, 4, strides: 2, padding: true)
    @cvt2 = Conv2DTranspose.new(base_num_filters, 4, strides: 2, padding: true)
    @bn1 = BatchNormalization.new
    @bn2 = BatchNormalization.new
    @bn3 = BatchNormalization.new
    @bn4 = BatchNormalization.new
    @bn5 = BatchNormalization.new
    @bn6 = BatchNormalization.new
    @bn7 = BatchNormalization.new
    @bn8 = BatchNormalization.new
  end

  def forward(x)
    input = InputLayer.new(@input_shape).(x)
    x = @cv1.(input)
    x = @bn1.(x)
    h1 = LeakyReLU.(x, 0.2)

    x = @cv2.(h1)
    x = @bn2.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cv3.(x)
    x = @bn3.(x)
    h2 = LeakyReLU.(x, 0.2)

    x = @cv4.(h2)
    x = @bn4.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cv5.(x)
    x = @bn5.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cvt1.(x)
    x = @bn6.(x)
    x = LeakyReLU.(x, 0.2)
    x = Concatenate.(x, h2, axis: 3)

    x = @cv6.(x)
    x = @bn7.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cvt2.(x)
    x = @bn8.(x)
    x = LeakyReLU.(x, 0.2)
    x = Concatenate.(x, h1, axis: 3)

    x = @cv7.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cv8.(x)
    x = Tanh.(x)
    x
  end
end

class Discriminator < Model
  def initialize(input_shape, base_num_filters)
    super()
    @input_shape = input_shape
    @cv1 = Conv2D.new(base_num_filters, 4, padding: true)
    @cv2 = Conv2D.new(base_num_filters, 4, strides: 2, padding: true)
    @cv3 = Conv2D.new(base_num_filters * 2, 4, padding: true)
    @cv4 = Conv2D.new(base_num_filters * 2, 4, strides: 2, padding: true)
    @d1 = Dense.new(1024)
    @d2 = Dense.new(1)
    @bn1 = BatchNormalization.new
    @bn2 = BatchNormalization.new
    @bn3 = BatchNormalization.new
    @bn4 = BatchNormalization.new
  end

  def forward(x)
    x = InputLayer.new(@input_shape).(x)
    x = @cv1.(x)
    x = @bn1.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cv2.(x)
    x = @bn2.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cv3.(x)
    x = @bn3.(x)
    x = LeakyReLU.(x, 0.2)

    x = @cv4.(x)
    x = @bn4.(x)
    x = LeakyReLU.(x, 0.2)

    x = Flatten.(x)
    x = @d1.(x)
    x = LeakyReLU.(x, 0.2)

    x = @d2.(x)
    x
  end

  # Discriminatorの学習を許可する。
  def enable_training
    trainable_layers.each do |layer|
      layer.trainable = true
    end
  end

  # Discriminatorの学習を禁止する。
  def disable_training
    trainable_layers.each do |layer|
      layer.trainable = false
    end
  end
end

class DCGAN < Model
  attr_reader :gen1
  attr_reader :gen2
  attr_reader :dis

  def initialize(gen1, gen2, dis)
    super()
    @gen1 = gen1
    @gen2 = gen2
    @dis = dis
  end

  def forward(input)
    images = @gen1.(input)
    @dis.disable_training
    # 変換した画像に対するDiscriminatorの出力結果。
    out = @dis.(images)
    # 変換した画像を元の画像に復元した画像。
    cycle_image = @gen2.(images)
    [cycle_image, out]
  end
end

# 学習したモデルを保存するためのモデル
class CycleGANModel < Model
  attr_accessor :dcgan_A
  attr_accessor :dcgan_B

  def initialize(dcgan_A, dcgan_B)
    super()
    @dcgan_A = dcgan_A
    @dcgan_B = dcgan_B
  end
end

モデルの作成

gen_A = Generator.new([64, 64, 3], 64)
gen_B = Generator.new([64, 64, 3], 64)
dis_A = Discriminator.new([64, 64, 3], 64)
dis_B = Discriminator.new([64, 64, 3], 64)
# リンゴからオレンジに変換するDCGAN-A
dcgan_A = DCGAN.new(gen_A, gen_B, dis_A)
# オレンジからリンゴに変換するDCGAN-B
dcgan_B = DCGAN.new(gen_B, gen_A, dis_B)
dis_A.setup(Adam.new(alpha: 0.00001, beta1: 0.1), SigmoidCrossEntropy.new)
dis_B.setup(Adam.new(alpha: 0.00001, beta1: 0.1), SigmoidCrossEntropy.new)
dcgan_A.setup(Adam.new(alpha: 0.0001, beta1: 0.5),
              [MeanAbsoluteError.new, SigmoidCrossEntropy.new], loss_weights: [10, 1])
dcgan_B.setup(Adam.new(alpha: 0.0001, beta1: 0.5),
              [MeanAbsoluteError.new, SigmoidCrossEntropy.new], loss_weights: [10, 1])
cycle_gan_model = CycleGANModel.new(dcgan_A, dcgan_B)

イテレータ

Cycle-GANでは、変換元画像と変換先画像がペアになっている必要はありません。
むしろペアになっていることで、過学習してしまう可能性があります。
そのため、ペアにならない変換元画像と変換先画像のミニバッチを返すイテレータを作成します。

class DNN::CycleGANIterator < DNN::Iterator
  def initialize(x_datas, y_datas, random: true, last_round_down: false)
    @x_datas = x_datas
    @y_datas = y_datas
    @random = random
    @last_round_down = last_round_down
    num_datas1 = x_datas.is_a?(Array) ? x_datas[0].shape[0] : x_datas.shape[0]
    num_datas2 = y_datas.is_a?(Array) ? y_datas[0].shape[0] : y_datas.shape[0]
    if num_datas1 < num_datas2
      @num_datas = num_datas1
    else
      @num_datas = num_datas2
    end
    reset
  end

  def next_batch(batch_size)
    raise DNN::DNNError, "This iterator has not next batch. Please call reset." unless has_next?
    if @indexes1.length <= batch_size
      batch_indexes1 = @indexes1
      batch_indexes2 = @indexes2
      @has_next = false
    else
      batch_indexes1 = @indexes1.shift(batch_size)
      batch_indexes2 = @indexes2.shift(batch_size)
    end
    x_batch, _ = get_batch(batch_indexes1)
    _, y_batch = get_batch(batch_indexes2)
    # ランダムにサンプリングした変換元画像と変換先画像を返す。
    [x_batch, y_batch]
  end

  def reset
    @has_next = true
    @indexes1 = @num_datas.times.to_a
    @indexes2 = @num_datas.times.to_a
    if @random
      @indexes1.shuffle!
      @indexes2.shuffle!
    end
  end
end

学習部分

モデルの学習を行う部分です。学習させている内容をざっくりとまとめるとこんな感じです。
リンゴ => オレンジの変換の学習
①Discriminator-Aの学習: Generator-Aが出力したオレンジの画像と本物のオレンジの画像を見分けられるように学習する。
②Generator-Aの学習: Discriminator-Aを騙せるようなオレンジの画像を出力できるように学習する。
③Generator-AとGenerator-Bの学習: Generato-Aが出力したオレンジの画像をGenerator-Bに渡したとき、出力される画像が、元のリンゴの画像に戻ることを学習させる。
※オレンジ => リンゴの場合についても、上記と同様に学習させる。

iter1 = DNN::CycleGANIterator.new(x, y)
iter2 = DNN::CycleGANIterator.new(x, y)
iter3 = DNN::CycleGANIterator.new(x, y)
iter4 = DNN::CycleGANIterator.new(x, y)
num_batchs = iter1.num_datas / batch_size
real = Numo::SFloat.ones(batch_size, 1)
fake = Numo::SFloat.zeros(batch_size, 1)

(initial_epoch..epochs).each do |epoch|
  num_batchs.times do |index|
    x_batch, y_batch = iter1.next_batch(batch_size)
    x_batch2, y_batch2 = iter2.next_batch(batch_size)
    x_batch3, y_batch3 = iter3.next_batch(batch_size)
    x_batch4, y_batch4 = iter4.next_batch(batch_size)

    # DCGAN-Aの学習
    images_A = gen_A.predict(x_batch)
    dis_A.enable_training
    dis_loss = dis_A.train_on_batch(y_batch, real)
    dis_loss += dis_A.train_on_batch(images_A, fake)

    dcgan_loss = dcgan_A.train_on_batch(x_batch2, [x_batch2, real])

    puts "A epoch: #{epoch}, index: #{index}, dis_loss: #{dis_loss}, dcgan_loss: #{dcgan_loss}"

    # DCGAN-Bの学習
    images_B = gen_B.predict(y_batch3)
    dis_B.enable_training
    dis_loss = dis_B.train_on_batch(x_batch3, real)
    dis_loss += dis_B.train_on_batch(images_B, fake)

    dcgan_loss = dcgan_B.train_on_batch(y_batch4, [y_batch4, real])

    puts "B epoch: #{epoch}, index: #{index}, dis_loss: #{dis_loss}, dcgan_loss: #{dcgan_loss}"
  end
  if epoch % 5 == 0
    cycle_gan_model.save("trained/cycle_gan_model_epoch#{epoch}.marshal")
  end
  iter1.reset
  iter2.reset
  iter3.reset
  iter4.reset
end

モデルの実行

今回も学習済みモデルのGeneratorを用意しています。
Githubからリポジトリをクローンして、「imgen.rb」を実行することで、画像を生成することができます。

$ ruby imgen.rb

実行結果

生成した画像のうち、比較的うまくいったものを貼っておきます。
inputが入力した画像で、outputが変換した画像になります。
上2行が、リンゴをオレンジに変換したもので、下2行は、オレンジをリンゴに変換したものです。

オレンジ色のリンゴと赤色のオレンジができただけなので、Cycle-GANに成功したとは言えそうにないですが、CPUでやったにしてはうまくいったんじゃないでしょうか。
cycle-gan.PNG

おわりに

Pix2pixが動いたので、もしかしたらCycle-GANもいけるんじゃね?っていう軽い気持ちでやってみましたが、思ってたよりはうまく動いてくれました。
いつかは、ruby-dnnをGPUに対応させて、ウマをシマウマに変換させられるようになりたいですね!


  1. Pix2pixモデルを使いまわしているので、InstanceNormalizationを使っていないなど、実際のCycle-GANとはいくつか異なる点があります。 

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

GraphQLのクエリを自動的にテストする

こんにちは。株式会社ビットジャーニーでエンジニアをしてる@pockeです。
ビットジャーニーではKibelaというWebアプリケーションを開発しています。

KibelaではPublicなWeb APIをGraphQLを使用して提供しています。
https://github.com/kibela/kibela-api-v1-document
また、このAPIはKibelaのWebアプリケーションの内部でも同じものが使用されています。

先日、このGraphQL APIを自動的に(ある程度)網羅的にテストするためのgem graphql-autotestをリリースしました。
https://github.com/bitjourney/graphql-autotest
この記事ではこのgraphql-autotestを紹介します。

なおgraphql-autotestはRubyで書かれていますが、任意の言語で実装されたGraphQLサーバーをテスト可能です(Rubyのコードを多少書く必要はありますが)。

「網羅的なテスト」とは

graphql-autotestの目的は、GraphQLのクエリをある程度網羅的にテストすることです。

私達は普段GraphQL APIを開発する際にユニットテストを書いています。
理想論を言えばすべてのフィールドにユニットテストを書きたいところですが、しばしば漏れが生じます。

graphql-autotestはそのような漏れを拾い上げるようなテストです。
まず、スキーマ定義から可能な限りすべてのフィールドを取得するようなクエリを自動的に書き出します。
そしてそのクエリを実行し、エラーが出ないことを確認します。

ポイントは、クエリを自動的に書き出す点です。
クエリの生成を自動化することで、新規に追加したフィールドにユニットテストを書き忘れていたとしても、graphql-autotestでそのフィールドをテストできます。

ただし、デフォルトではgraphql-autotestはクエリの実行がエラーを出さないことのみをチェックします。
「GraphQLサーバーの実装にtypoがあってNoMethodErrorが出ていた」と言ったケースは検出できますが、「1が返るべきところで2が返っていた」と言ったバグは検出できません。
ただし、クエリを実行したレスポンスに対して、それを検証する適当なコードを書いてやればある程度はテストできるでしょう。

なお、クエリの実行はgraphql gemを使っている場合にのみ可能です。Ruby以外の言語でGraphQLサーバーが実装されている場合は、生成されたクエリを実行する部分は別に実装する必要があります。

使い方

では、使い方を見ていきましょう。

Installation

まずこのgemをインストールします。

$ gem install graphql-autotest

もしくはBundlerを使っている場合、次のコードをGemfileに足してください。

Gemfile
gem 'graphql-autotest'

設定例

graphql gemを使っている場合

まずは、graphql gemを使っている場合の一番シンプルなコードを紹介します。

require 'graphql/autotest'

class YourSchema < GraphQL::Schema
end

runner = GraphQL::Autotest::Runner.new(
  schema: YourSchema,
  context: { current_user: User.first },
)

runner.report!

まず、YourSchemaとしてスキーマ定義をしています。
これは自身のスキーマ定義に置き換えてください。

そして、GraphQL::Autotest::Runnerのインスタンスを生成し、report!でクエリを生成、実行しています。
Runner.newの引数には定義したYourSchemaと、contextを渡しています。このcontextYourSchema.executeを呼ぶ際に渡されます。

これだけでスキーマ定義からクエリを自動生成して、実行し、エラーがないかをテストできます。
ただし、実用上ではデフォルトの設定では充分な結果が得られないことがあるでしょう。そのため後述するarguments_fetcherなどの設定が必要です。

graphql gemを使っていない場合

冒頭でも説明したとおり、このgemはgraphql gemを使ってサーバーを実装している場合以外でもクエリの自動生成ができます(クエリの実行はできません)。

クエリを自動生成するには、 https://github.com/kibela/kibela-api-v1-document/blob/master/schema.graphql のようなスキーマ定義が必要です。
このスキーマ定義がpath/to/schema.graphqlに保存されている場合、次のように書くとクエリを生成できます。

require 'graphql/autotest'

fields = GraphQL::Autotest::QueryGenerator.from_file(path: 'path/to/schema.graphql')

# 生成したクエリがすべて表示される
fields.each do |field|
  puts field.to_query
end

生成したクエリは適応なGraphQLクライアントに渡して実行すると良いでしょう。

設定方法

ここまでで、デフォルトの設定でクエリを生成、実行する方法を解説しました。
ですが前述したとおり、デフォルトの設定では不十分なケースもあります(Kibelaの場合はそうです)。
そのためgraphql-autotestではいくつかの設定項目を用意しています。

以下にその設定項目を解説します。

arguments_fetcher

arguments_fetcherでは、フィールドが引数を受け取る場合にどのような引数を渡すかを定義します。
arguments_fetcherはProcを指定します。指定したProcはフィールドの情報を引数として呼ばれ、Hashを返せばそれが引数として使われます。

デフォルトではgraphql-autotestでは必須の引数がない(== 引数なしで呼べる)フィールドのみをクエリとして書き出します。
そのため、必須の引数があるフィールドを取得するためにはarguments_fetcherを使用して、どのような引数を渡したら良いかを定義します。
また必須でない引数でも、必要があれば指定できます。

たとえば、KibelaではRelay Connectionに対するfirstlast引数が必須となっています。
このfirstを埋めるようなfetch_argumentsの定義は次のようになります。

fill_first = proc do |field|
  field.arguments.any? { |arg| arg.name == 'first' } && { first: 5 }
end

fields = GraphQL::Autotest::QueryGenerator.from_file(
  path: 'path/to/schema.graphql',
  arguments_fetcher: GraphQL::Autotest::ArgumentsFetcher.combine(
    fill_first,
    GraphQL::Autotest::ArgumentsFetcher::DEFAULT,
  ),
)

このarguments_fetcherをより設定することで、フィールドの網羅性を上げることができます。

skip_if

skip_ifでは、取得したくないフィールドをスキップできます。
どうしても自動的なクエリの生成ではエラーが出てしまうようなフィールドを無視するのに使えるでしょう。

skip_ifもProcを指定します。このProcはフィールドの情報を引数に呼ばれ、truthyな値を返せばそのフィールドはスキップされます。

skip_if = proc do |field|
  field.name == 'sensitiveField'
end

fields = GraphQL::Autotest::QueryGenerator.from_file(
  path: 'path/to/schema.graphql',
  skip_if: skip_if,
)

max_depth

max_depthには、クエリの最大の深さを指定します。

graphql-autotestではフィールドの循環参照があった場合に無限ループにならないよう制御しているため、理論上はmax_depthを指定しなくてもクエリの実行は終了します。
とはいえクエリが深すぎる場合、クエリの生成や実行に時間がかかってしまいます。
その場合はmax_depthオプションを使用して、深さを減らしてください。

デフォルトのmax_depthは10です。この値は適当です。

fields = GraphQL::Autotest::QueryGenerator.from_file(
  path: 'path/to/schema.graphql',
  max_depth: 5,
)

まとめ

GraphQLの網羅的なテストを実行するためのGemであるgraphql-autotestを紹介しました。

実際にKibelaではこのgemを実装する過程で複数のバグを発見しました。
バグがあったのはいずれもKibelaのWebアプリケーションでは使っていないフィールドでした。ですが、ユーザーに公開されているAPIでも取得されうるフィールドであることを考えると早めにバグを発見できたのは良かったと思います。

graphql-autotestを使って、GraphQLサーバーのテストを実施していただければ幸いです。

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

【1日目】今日の学び fontawesomeをeach文の中で繰り返して表示させる

【1日目】プログラミング初学者の今日の学び
プログラミング初学者が、ポートフォリオを作っている中で得た気づきを書いていきます。

Fontawesomeをhtml.erbのループ内で使う

Microposts一覧表示ページにおいて、各micropostを削除するボタンを、Fontawesomeのゴミ箱アイコンで表現したかった。
埋め込みrubyの繰り返し内でhtmlタグを使いたい時は、ブロックを受け渡せば良いらしい。

app/views/microposts/_micropost.html.erb
<% @microposts.each do |micropost| %>
  <div>
    <%= link_to micropost.name, "#" %>
      <%= link_to micropost_path(micropost), :method => :delete do %>
      <i class="far fa-trash-alt"></i>
    <% end %>
  </div> 
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【1日目】初学者の今日の学び ストロングパラメータ, fontawesomeについて

【1日目】プログラミング初学者の今日の学び
プログラミング初学者が、ポートフォリオを作っている中で得た気づきを書いていきます。

Fontawesomeをhtml.erbのループ内で使う

Microposts一覧表示ページにおいて、各micropostを削除するボタンを、Fontawesomeのゴミ箱アイコンで表現したかった。
埋め込みrubyの繰り返し内でhtmlタグを使いたい時は、ブロックを受け渡せば良いらしい。

app/views/microposts/_micropost.html.erb
<% @microposts.each do |micropost| %>
  <div>
    <%= link_to micropost.name, "#" %>
      <%= link_to micropost_path(micropost), :method => :delete do %>
      <i class="far fa-trash-alt"></i>
    <% end %>
  </div> 
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Progate無料版をやってみる【Ruby on Rails5 I】

前回に引き続き無料レッスンをこなしていきたいと思います。

Rubyとどう違うのか?
→Rubyのフレームワークのことを指す、またはRubyのフレームワークを使って開発することだと思います。
→Webアプリケーション開発のためのフレームワークらしいです。
Ruby on Railsとフレームワーク

Ruby on Railsの全レッスンをクリアすると「TweetApp」というTwitterに似せたWebアプリを作れるらしいです。
ちょっと興味あるので、課金しようかな・・・。

というか経験者の場合、課金しないとどのレッスンも有益な知識は得られないかもですね・・・。
無料だから仕方ないですね!

Ruby on Rails5 I

公式レッスン

Ruby on Railsを始めよう

・まずRailsをインストール

gem instrall rails 

適当なディレクトリを作り
image.png

ターミナルで、そのディレクトリまで移動し以下を入力実行。

rails new tweet_app

image.png

勝手にいろいろなディレクトリやファイルが作られました!
tweet_appディレクトリがTOPにできますので、自身でディレクトリを用意する必要はなかったですね・・・。

・tweet_appのディレクトリに移動して、

rails server

を実行したところ以下のエラー
image.png

bundleってなにw
参考にさせていただきました。
https://pikawaka.com/rails/bundler
bundle installを行うと、Gemfileに記載されたgemがインストールされるとのこと。
Gemfileにsqlite3があった。
image.png

これのバージョンが1.4だとだめらしい。
以下を参考にさせていただきました。
Windows10でRuby on Railsの環境構築

1.3.13に変更しました。
image.png
一度bundle updateした後
image.png

改めてrails server
image.png

あれwまだエラー・・・。

以下の「3.sqlite3のダウンロード」の手順を実行
Windows10でRuby on Railsの環境構築

再度rails server
→ まったく同じエラー。

以下を参考にさせていただきました。
Windows10で「rails server」コマンドを実行したときに「cannot load such file -- sqlite3/sqlite3_native」とエラーが出ることへの対処

上記対応を行い、改めてrails server
以下のエラー(もうなんなの・・・)
image.png

rails webpacker:installを実行する必要があるとのこと。
ただ、それに加えてNode.jsYarnも付随してインストールが必要であった・・・。
参考にさせていただきました。
https://qiita.com/libertyu/items/1eb74adc817ab8971100

上記をすべて行い、rails server
image.png

あ、なんかいい感じ。

http://localhost:3000をブラウザで表示してみると
image.png
なんだこれは・・・
メッセージを見ると、sqlite3が1.4だけど1.3.13が指定されているよー的な感じ。

ちょっと心当たりがありました。
少し上の対応で
Windows10で「rails server」コマンドを実行したときに「cannot load such file -- sqlite3/sqlite3_native」とエラーが出ることへの対処

sqlite3_native.soのコピーの時にC:\Ruby26-x64\lib\ruby\gems\2.6.0\gems\sqlite3-1.4.2\lib\sqlite3\sqlite3_native.soをコピーしており(というか1.13.3のディレクトリがなかったので)、Gemfileの指定(1.4)とは異なると違和感を感じていました。

よってGemfileのsqlite3のバージョンを1.13.3 → 1.4にもどしたところ
image.png

image.png
やっと正常に表示できました。
結局1.13.3に戻した意味がなかった・・・。

トップページを作ってみよう

・以下コマンドでTOPがページが作成される

rails generate controller home top

http://localhost:3000/home/topをブラウザで開くと
image.png

ビューを学ぼう・ビューを理解しよう

・先ほどのTOPページはapp/views/home/top.html.erbに作成されているらしい。
image.png
このページをリクエスト時にクライアント側に返している

コントローラを理解しよう

・先ほどのrails generate controller home topapp/controllers/home_controller.rbのファイルにtopというメソッドが作られる
image.png
・「コントローラと同じ名前のビューフォルダから、アクションと同じ名前のHTMLファイルを探してブラウザに返します」とのこと。
例:URLで~home/topと指定すると、homeの下のtop.html.erbを返す。

ルーティングを学ぼう

ルーティングとは
config/routes.rb
image.png
home/topの記載がありました。
ここの記述を変更することで、指定したURLと開くページの対応関係を変更できる。

サービス紹介ページを追加しよう

config/routes.rbapp/controllers/home_controller.rbに追記することで、新たにルーティングとメソッドを追加できる。
また、そのリクエストの帰り値となるhtmlをapp/views/homeディレクトリに新しく作成する。

レイアウトを整えよう

・CSSファイルはapp/assets/stylesheetsに配置されている。
 すべてのhtmlに対して適用される模様。

画像を表示しよう

・publicに画像を入れておくだけで、「<img src="/画像名" >」や「background-image: url("/画像名");」のように、画像名を指定するだけで、簡単に画像を表示することができる模様。

トップページのURLを変更しよう

・ルーティングにてget "/" => "home#top"とする

リンクを作ろう

・aタグのhrefにルーティングで設定したURLをそのまま入れることで対応するhtmlを表示できる。

クリアしました
image.png

感想

他言語(私が知っている数少ない言語)に比べて簡単にWebアプリが作成できるような仕組み、枠組みという印象を受けました。
→どれくらい簡単に凝った画面を作れるかも試してみたくなりました。

環境構築のところはかなり手こずってしまい、課題となりました。

次回はRuby on Rails5 IIをやっていきたいと思います。
無料はIIまでのようです。

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

rbenvでプロダクト毎にrubyのバージョン管理をする

rbenv

rubyをプロダクト毎にバージョンを管理する必要性は以下の2つにあると考えています。

  • ローカルを汚さない
  • 複数のプロダクトを落とす毎に毎度毎度バージョンを切り替えるのは大変

gemsetを併用して使うと尚、いいと推測します。下記のリンクはrbenv-gemsetの使い方です。
rbenv-gemsetを使ってプロジェクト毎のgemを管理する

rbenvをbrewでインストールする

$ brew installl rbenv

rbenvの基本コマンド群

インストールできるrubyのパッケージを確認
$ rbenv install -l

インストールしてあるrubyのバージョンを管理できる
$ rbenv versions

rbenvの初期化
$ rbenv init

rubyをインストールする
$ rbenv install バージョン

インストールしたrubyのバージョンを反映させる
$ rbenv rehash

システム全体のrubyバージョンを設定する
$ rbenv global rubyバージョン

プロダクト毎にrubyバージョンを設定する
$ rbenv local rubyバージョン

基本的なコマンドはざっと上記の通りです。

プロダクト毎にrubyバージョンを管理する

必要なrubyバージョンを手に入れる

$ rbenv install rubyバージョン

$ rbenv rehash

初期化する

$ rbenv init
# Load rbenv automatically by appending
# the following to ~/.zshrc:

eval "$(rbenv init -)"

initして出てきた内容を.zshrcに書き込み、反映させる

$ echo 'eval "$(rbenv init -)"' >> .zshrc

$ source ~/.zshrc

管理するプロダクトへ移動し、localへセットする

$ cd プロダクト名

$ rbenv local rubyバージョン

下記のようなファイルができているはずです
$ cat .ruby-version
2.5.4

rubyのバージョンが変わったことを確認する

プロダクト内部では下記のバージョンに
$ ruby -v
ruby 2.5.4p155 (2019-03-13 revision 67245) [x86_64-darwin19]

$ rbenv versions
  system
* 2.5.4 (set by /Users/naoya.ohsaki/gx_community_admin/.ruby-version)
  2.7.0

$ cd

プロダクトの外部では下記のバージョンに
$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

$ rbenv versions
* system (set by /Users/naoya.ohsaki/.rbenv/version)
  2.5.4
  2.7.0

上記のように切り替わっていたら完了です。お疲れ様でした。

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

Rails~パスワードの取り扱いについて~

パスワードの取り扱い

パスワード機能がついているSNSアプリやサービスにはデータベースにパスワードが保存されている。このままだとPCを覗き見されたりハッキングでデータを盗まれた場合そのパスワードを使って他人のアカウントに不正にログインできてしまう。そのため万が一パスワードを覗き見されたりデータを盗まれてもパスワードの内容がわからない状態を目標とする。

gemファイルを使用しパスワードを暗号化する。

gemとは

gemとはRubyやRailsでプログラミングをする際に「よく使う機能」をパッケージ化したもの。「検索機能を作るgem」や「暗号化するためのgem」など様々なgemが存在し、Railsにインストールすることで使用することができる。
今回使用するgem= bcrypt(暗号化)するためのgem

gemfiles

Railsにはインストールしたいgemを記述するGemfileというファイルが存在する。
「gem 'rails','5.0.3'」等と記載する。「rails new」コマンドで生成されたgemfileにはすでにいくつかのgemが書かれてインストールされている。数字の部分はgemのバージョンを意味する。バージョンを指定しない場合は最新のgemがインストールされる。

bcryptのインストール

Gemfile

gem 'bcrypt'

ターミナル

bundle install

Gemfileへインストールしたいgemを入力→ターミナルにてbundle installと入力する。
bcryptをインストールすると「has_secure_password」というメソッドが使える用になるパスワードを扱うモデルにhas_secure_passwordを追加する。これによりユーザーを保存する際に自動的にパスワードを暗号化してくれる。

password_digestカラムを追加する。

上記のメソッドは暗号化したパスワードをpassword_digestカラムに保存する。そのためpasswordカラムは不要となるので削除を行う。

マイグレーションファイルの作成

ターミナル

rails g migration change_users_columns
rails db:migrate

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

def change
   add_column :users,:password_digest,:string
  remove_column:users, :password, :string
end

authenticateメソッド

has_secure_passwordメソッドを有効にするとauthenticateメソッドを使える様になる。このメソッドは渡された引数を暗号化し、password_digestの値と一致するかどうかを判定してくれる。authenticateメソッドを使用し「送信されたメールアドレスと一致するユーザー」のパスワードと送信されたパスワードが一致するかどうかログイン処理を行う。

controller

def login
  @user = User.find_by(email:params[:email])
if @usergets && @user.authenticate(params[:password]) && @user.authenticate(params[:password])
end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails ~いいね機能を作る~

テーブルの作成

モデルとマイグレーションファイルの作成を準備する。今回作成するテーブルにはuser_idとpost_idの2つのデータをもたせるように処理を行う。
STEP1 マイグレーションファイルを作成
STEP2 マイグレーションファイルの内容をデータベースに反映させる

ターミナル

マイグレーションファイルの作成
rails g model Like user_id:integer post_id:integer
データベースに変更を反映。データ名:integerでデータ作成
rails db:migrate

バリデーションの追加

class LIKE<ApplicationRecord
  validates:user_id,{presence:true}
  validates:post_id,{presence:true}
     どちらも存在しないと不完全なデータとなってしまう。
end

コンソールにて仮作成

rails console
  > like=Like.new(user_id:1,post_id:2)
IDが「1」のユーザーがIDが「2」の投稿をいいねした、というデータを作成する。
  > like.save

いいねした投稿かどうかを表示する

ログインしているユーザーがその投稿にいいね舌データが存在するという条件のために、user_idとpost_idが合致するデータがlikesテーブルに存在するかどうか、find_byを用いてチェックする(find_by には該当する条件が存在しなかったときにNILを返す性質がある。)

view

<% if Like.find_by(user_id:@current_user.id,post_id:@post.id)%>
   いいね済み
<%else%>
  いいね!していません
<%end%>

いいねボタンの準備

いままでのコントローラーは「rails g controller」コマンドで自動生成してきた。コマンドを用いるとviewファイルも自動生成されるが今回はそれらのファイルが必要ないため手動でのコントローラ作成を行う。[likes_controller]を作成する。

createアクションを用意

作成したlikesコントローラに新たにLikeデータを作成するためにcreateアクションを用意する。

routes

Rails.applicaion.routes.draw do
  post"likes/:post_id/create"=>"likes#create"
end

controller

def create
   @like = Like.new(
     user_id:@current_user.id,
     post_id:params[:post_id]
   )
   @like.save
   redirect_to("/posts/#{params[:post_id]}")
end

view

<%Like.find_by(...)%>
  <%= link_to("いいね!済み","/likes/#{@post/id}/destroy",)%>
<% else %>
 <%=link_to("いいね!","/likes/#{@post.id}/create",{method:"post"})%>
<% end %>

いいね!を削除する

destroyアクションの用意

「いいね!」を取り消す機能を作るために、まずはlikesコントローラにdestroyアクションを作成する。destroyアクション内では、受け取った@current_user.idとparams[:post_id]をもとに削除すべきlikeデータを取得しdestroyメソッドを用いて削除する。

controller

def destroy
   @like = Like.find_by(
     user_id:@current_user.id,
     post_id:params[:post_id]
    )  
   @like.destroy
   redirect_to("/posts/#{params[:post_id]}")
end

routes

Rails.application.routes.draw do
  post"likes/:post_id/destroy"=>"likes#destroy"
end

countメソッド

likesテーブルからデータの件数を取得するにはcountメソッドを用いる。
countメソッドは配列の要素数を取得するメソッドですがテーブルのデータ数を取得するためにも利用することができる。

likesアクションを用意する

routes

get"users/:id/likes"=>"users#likes"

controller

def likes
  @user = User.find_by(id:params[:id])
  @likes = Likes.where(user_id: @user.id)
end

view

  <% @likes.each do |like|%>
    <% post = Post.find_by(id:like.post_id)%>
  <% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者向け、メソッドの用語まとめ(Ruby)

はじめに

  • 本記事はRubyでプログラミングを初めてメソッドでいろんな用語が出てきて混乱しているという方が調べる際のヒントになればいいなと思い書きました。
  • まずい表現や間違ったことが書いてある場合はコメント等で教えていただけると幸いです。

メソッドってなんだ?
引数、仮引数ってなんだ?
戻り値ってなんだ?、、、
となった人向けに書きました。

私はなりました。

メソッドがプログラミングを始めて、初めのつまづきポイントなのではないかなと思っています。

メソッドのおさらい

まずメソッドのおさらいです。
実際のコードを見てみましょう。
例として、税込価格を計算して返してくれるメソッドを書いてみました。

####################メソッドを定義############################
#税込価格を計算するメソッドを定義
def calculate_tax_included_price(price)
  #税込価格 tax_included_priceを変数定義
  #税込価格の計算結果を代入
  tax_included_price = price * 1.1

  #returnで戻り値を返す(省略可能)
  return tax_included_price
end

####################メソッドを呼び出す######################
#支払い額 paymentを変数定義
#calculate_tax_included_price 引数:500 でメソッドを呼び出す。
payment = calculate_tax_included_price(500)

#putsメソッドでpaymentをターミナルに出力
puts payment

####################出力結果(コンソール)####################
550.0
  • def ~ end のかたまりがメソッドの定義です。 defは定義 definitionの略
  • メソッドは呼び出す場所より上で定義する。(Rubyでは上から読み込んでいくため、メソッドは使用される前に定義する必要があるため。)
  • returnの一文は省略が可能

メソッドの用語

メソッドに入ってから用語が増えるので、用語を覚えるまでは混乱することが多いと思います。
ここで復習しましょう。

・ メソッド、関数

メソッドと関数という言葉がほとんど同じ意味なのに2通りの呼び方があることが混乱の一因だと思います。

メソッドと関数は厳密には違う意味をもつらしいのですが区別しなくても、あまり困ることはないかなと思います。
Rubyではメソッドの概念しか登場しないそうなのでRubyでは全部メソッドと呼んでおけばOKだと思います。

・ メソッドの定義

def ~ endの中に処理を書いて、呼び出して使えるようにすることです。

・ メソッドを呼び出す、使う

image.png

メソッド名を記述する、メソッドに仮引数が定義されている場合は引数を渡すとメソッド内の処理が実行されます。

・ (実)引数、仮引数

どっちがどっちかわからなくなる、引数と仮引数ですがここで復習しましょう。
image.png
メソッドを呼び出すところで、渡すのが(実)引数
メソッドを定義したときに書いているのが仮引数です。

ちなみに、実引数と仮引数の変数名違う名前でもOKです。ここは混乱するポイントだと思いますが、
実引数が仮引数に改名されると考えるのではなく、
仮引数はメソッドが呼ばれたときに新しく定義される変数で、実引数の値が仮引数にコピーして格納されるとイメージしましょう。

・ 戻り値、返り値

これも2通りの呼び方がある戻り値、返り値ですが、全く同じものです。好きな方で呼びましょう
image.png

returnの後ろに書いたものが戻り値、返り値として呼び出し元に返されます。

image.png
calculate_tax_included_price(500)が550.0という値が格納された変数tax_included_priceに置き換わると考えましょう。

Rubyではreturnを省略するとメソッドの最後に記述した式が戻り値として返されます。(今回の例ではreturnを省略しても挙動に変化なし)

・ レシーバ

"こんにちは".size  #=> 5

メソッドの実行主体のこと、「 . 」の左側

なぜレシーバという名前かというと、、、
オブジェクト指向では処理をメッセージのやり取りによって行うという考え方があり、メソッドの実行側がこのメッセージの受け取り側と捉えることができます。そのためレシーバと呼ばれます。
だそうです。

・ (カーネル)メソッド、組み込み関数

標準で組み込まれているメソッド・関数
レシーバが不要なメソッド
よく使うputsメソッドなどが含まれます。

・ 公開範囲

Rails
 private 
  def set_user
    @user = User.find(params[:id])
  end

public, protected, privateがある。(protectedはあまり使われない)
・public どこからでもアクセス可能、デフォルト設定でされる。
・private 外部からアクセス不可、隠蔽したいメソッドに利用する。

・ 継承

子クラスは継承親のメソッドを受け継ぎます。

########## 親クラス runメソッドを定義################################
class Car
 #runメソッド
 def run
   puts '走る'
 end
end

######### 子クラス(Carクラスを継承している) メソッドは何も定義していない######
class SuperCar < Car

end

####################実行部分###################################
#SuperCarクラスのインスタンス化を行う。
supercar = SuperCar.new
#SuperCarではrunメソッドを定義していないがCarクラスのrunメソッドを使用できる。
supercar.run 

####################出力結果(コンソール)##########################
走る

・ オーバーライド

子クラスで親クラスで定義されたものと同じ名前を使ってメソッドを新しく定義しなおすことです。

########## 親クラス runメソッドを定義################################
class Car
 #runメソッド
 def run
   puts '走る'
 end
end

#########子クラス(Carクラスを継承している) runメソッドをオーバーライドしている####
class SuperCar < Car
 def run
   puts '超速く走る'
 end
end

####################実行部分###################################
#SuperCarクラスのインスタンス化を行う。
supercar = SuperCar.new
#SuperCarのオーバーライドされたrunメソッドを使用する。
supercar.run 

####################出力結果(コンソール)##########################
超速く走る

・ オーバーロード、多重定義

Rubyにはない概念ですが、オーバーライドと字が似ているので誰しもが最新は混乱すると思います。
どういうものかというとメソッドの名前は同じで引数のデータ型や引数の個数を変えたものを複数定義するというものです 。
同じメソッド名が使えるのでメソッドを使うときに楽になる、ポリモーフィズムが実現できると言ったメリットがあります。

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

RuboCopのAbcSizeチェックの対応

Rubyの静的コード解析をやってくれるRuboCopをRails開発に導入しました。そこで発生したAbcSizeチェックについての作業メモ。

AbcSizeとは

実装したメソッドに対してRuboCop君が計算してくれるスコア。
以下の3つのカウントが評価対象。

  • Assignment : 代入
  • Branch : メソッド呼び出し
  • Condition : 条件

デフォルトで15を超えると警告が出て、もっとスコア下げてねと言われる。

警告が出たメソッド

ユーザーのサインイン機能を担う、こちらのSessionsController.create

sessions_controller.rb
def create
  @user = User.find_by(email: params[:session][:email].downcase)
  if @user&.authenticate(params[:session][:password])
    # ユーザーログイン後にユーザ情報のページにリダイレクトする
    log_in @user
    params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
    recdirect_back_or @user
  else
    # エラーメッセージを作成する
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
  end
end

出た警告はこちら。

Assignment Branch Condition size for create is too high. <2, 19, 4> 19.52/15",

どうやらスコアは19.52らしく、評価項目毎だとこうなってる。

  • Assignment => 2
  • Branch => 19
  • Condition => 4

対策

...とはいえ現状可読性は問題ない(主観)し、メソッド抽出が必要なほどロジックは複雑ではないので、AbcSizeチェックの基準値を調整しておく。

.rubocop.yml
Metrics/AbcSize:
  Max: 20

おわりに

デフォルトの15を下回れるような解決方法があれば後ほど更新したい。

ちなみにRuboCop自体の.rubocop_todo.ymlでは基準値が18になっているという...

https://github.com/rubocop-hq/rubocop/blob/b10f7b63b2d1b4d71cc80661e30db569e7079237/.rubocop_todo.yml#L9-L11

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

【今日のエラー】クラスとインスタンス

railsの勉強の際、あまりにもインスタンスの理解が浅かったのでRubyの復習をしてきました。

クラスとインスタンス

簡単にまとめると、クラスという設計図をまず作ることによってその設計図からいくらでもインスタンスを生成することができる。

イメージとしては設計図から大量の実物が生成されるイメージ。
例えば

ruby_test.rb
class Tanaka
attr_accessor :name, :age

def initialize
    self.name = '田中'
    self.age = 10
end


  def introduce(personal_name)
    puts "#{self.name}#{personal_name}を紹介して#{self.age}歳という年齢を教えた"
  end
end

tanaka_A = Tanaka.new

tanaka_A.introduce('佐藤')

p tanaka_A

結果
田中が佐藤を紹介して10歳という年齢を教えた

日本語的には非常に怪しいですが笑

田中というクラスから名前や年齢などのインスタンスが生成されそれをtanaka_A.introduce()とすることでintroduceメソッドを呼び出したりすることもできる。
かなり直感的に理解しやすかったです。

またこのself.の部分は下で書いたtanaka_Aのインスタンスを指すということを理解しておくとなおよい。

間違ってたら指摘お願いします笑

参考文献

クラスメソッドの使い方まとめ

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

Rails勉強中 EC_siteカート機能 同一商品追加時のcreate

 はじめに

現在Rails勉強中の人が備忘録として書いてます。
こうした方がいい、あーした方がいいといった点があれば教えてください

 コード

スクリーンショット 2020-03-15 14.03.33.png

内容

viewは比較的スムーズに実装できると思うので割愛
コード見れば分かると思うが、自身が誤っていないかの確認

始めに、isExistとIdのデフォルト値の定義
each end内で追加した商品IDとカートに追加されている商品IDが同じかif文で記す
if isExistではeach内の変数を持っていけないので、前に定義したIDを持ってくる

sumで新たに追加したい値と既にカートに保存されている値を計算
updateするのは追加する商品の個数だけなので(ここではcount)、
update_attributesでカラム指定して実行
※update_attributeもあるが、これはvalidationチェックされずにupdateが実行される
個数をupdateしたいが、仮に文字が入ったとしてもupdateされてしまう。

最後に

modelとかhelperを使えばもう少しコントロールの記述量が減るかもしれないが、
勉強不足のため日々精進していきたい

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

FullcalendarをRailsに導入

Fullcalendarについて

カレンダー機能付きのアプリを作りたく調べてみました。
こんな感じのが出来ます。
UNADJUSTEDNONRAW_thumb_9.jpg

FullCalendarはjQueryのプラグインでrailsに導入する際は、gemで導入しました。
今回はScaffoldを使って簡単なEventモデルを作り、カレンダーに反映出来るところまでやります。

では、

1.Gemの追加

Gemfile.
gem 'jquery-rails', '4.3.3'
gem 'fullcalendar-rails'
gem 'momentjs-rails'

そして、
bundle install

2.application.jsに記述

app/assets/javascript/application.js
//= require jquery
//= require moment
//= require fullcalendar

$(function () {
    // 画面遷移を検知
    $(document).on('turbolinks:load', function () {
        // lengthを呼び出すことで、#calendarが存在していた場合はtrueの処理がされ、無い場合はnillを返す
        if ($('#calendar').length) {
            function eventCalendar() {
                return $('#calendar').fullCalendar({
                });
            };
            function clearCalendar() {
                $('#calendar').html('');
            };

            $(document).on('turbolinks:load', function () {
                eventCalendar();
            });
            $(document).on('turbolinks:before-cache', clearCalendar);

            $('#calendar').fullCalendar({
                events: '/events.json'
            });
        }
    });
});

一つ目の関数ではFullCalendarの設定を読み込み、二つ目の関数ではFullCalendarを削除します。
その下に呼び出すコードを2行書き最後にイベントを表示させるためのコードを書きます。

3.application.cssに記述

app/assets/stylesheets/application.css
...
 *= require_tree .
 *= require_self
 *= require fullcalendar
 */

これで、あとはviewのerbファイルに
<div id="calendar"></div>
と書き込むとカレンダーが表示されます。

4.イベントを表示する

Fullcalendarにイベントの情報を表示するには、JSONファイルを使ってあげます。
イベントにはタイトル、説明、開始日、終了日が必要です。今回はscaffoldで作りました。
rails generate scaffold Event title:string description:text start_date:datetime end_date:datetime
そして、
rails db:migrate

JSONを渡すために、Railsのjbuilderというものを使っていきます。scaffoldによってjson.jbuilderファイルが自動で作られています。
以下のコードを追加

app\views\events\index.json.jbuilder
json.array!(@events) do |event|
  json.extract! event, :id, :title, :description   
  json.start event.start_date   
  json.end event.end_date   
  json.url event_url(event, format: :html) 
end

こうすることで、{"id":"1", "title":"タイトル", "description":"説明", "start":"日付1", "end":"日付2", "url":"some_address"}のようにjsonファイルが作られ、カレンダーが読み込めるデータになります。
url項目があることで、カレンダーの予定にurlが埋め込まれ、クリックすると予定の詳細に飛ぶことができます。

現状、ルーティングはこのようになっています。

routes.rb
Rails.application.routes.draw do
  resources :events
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root 'events#index'
end

一旦、これで表示は出来るようになりました。
あとは、カレンダーのオプションで色々触ってみて下さい。

$('#calendar').fullCalendar({ });の{ }内にオプションを加えることで、カレンダーのタイトルをYYYY年MM月のようにしたり、レイアウト、時間の表示を設定できます。

公式ドキュメントには色々な使い方、機能が載っていますので確認してみて下さい。
FullCalendarの使い方

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

FullCalendarをRailsに導入

FullCalendarについて

カレンダー機能付きのアプリを作りたく調べてみました。
こんな感じのが出来ます。
UNADJUSTEDNONRAW_thumb_9.jpg

FullCalendarはjQueryのプラグインでrailsに導入する際は、gemで導入しました。
今回はScaffoldを使って簡単なEventモデルを作り、カレンダーに反映出来るところまでやります。

では、

1.Gemの追加

Gemfile.
gem 'jquery-rails', '4.3.3'
gem 'fullcalendar-rails'
gem 'momentjs-rails'

そして、
bundle install

2.application.jsに記述

app/assets/javascript/application.js
//= require jquery
//= require moment
//= require fullcalendar

$(function () {
    // 画面遷移を検知
    $(document).on('turbolinks:load', function () {
        // lengthを呼び出すことで、#calendarが存在していた場合はtrueの処理がされ、無い場合はnillを返す
        if ($('#calendar').length) {
            function eventCalendar() {
                return $('#calendar').fullCalendar({
                });
            };
            function clearCalendar() {
                $('#calendar').html('');
            };

            $(document).on('turbolinks:load', function () {
                eventCalendar();
            });
            $(document).on('turbolinks:before-cache', clearCalendar);

            $('#calendar').fullCalendar({
                events: '/events.json'
            });
        }
    });
});

一つ目の関数ではFullCalendarの設定を読み込み、二つ目の関数ではFullCalendarを削除します。
その下に呼び出すコードを2行書き最後にイベントを表示させるためのコードを書きます。
その他、説明は少しはしょります。

3.application.cssに記述

app/assets/stylesheets/application.css
...
 *= require_tree .
 *= require_self
 *= require fullcalendar
 */

これで、あとはviewのerbファイルに
<div id="calendar"></div>
と書き込むとカレンダーが表示されます。

4.イベントを表示する

Fullcalendarにイベントの情報を表示するには、JSONファイルを使ってあげます。
イベントにはタイトル、説明、開始日、終了日が必要です。今回はscaffoldで作りました。
rails generate scaffold Event title:string description:text start_date:datetime end_date:datetime
そして、
rails db:migrate

JSONを渡すために、Railsのjbuilderというものを使っていきます。scaffoldによってjson.jbuilderファイルが自動で作られています。
以下のコードを追加

app\views\events\index.json.jbuilder
json.array!(@events) do |event|
  json.extract! event, :id, :title, :description   
  json.start event.start_date   
  json.end event.end_date   
  json.url event_url(event, format: :html) 
end

こうすることで、{"id":"1", "title":"タイトル", "description":"説明", "start":"日付1", "end":"日付2", "url":"some_address"}のようにjsonファイルが作られ、カレンダーが読み込めるデータになります。
url項目があることで、カレンダーの予定にurlが埋め込まれ、クリックすると予定の詳細に飛ぶことができます。

現状、ルーティングはこのようになっています。

routes.rb
Rails.application.routes.draw do
  resources :events
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root 'events#index'
end

一旦、これで表示は出来るようになりました。
あとは、カレンダーのオプションで色々触ってみて下さい。

$('#calendar').fullCalendar({ });の{ }内にオプションを加えることで、カレンダーのタイトルをYYYY年MM月のようにしたり、レイアウト、時間の表示を設定できます。

公式ドキュメントには色々な使い方、機能が載っていますので確認してみて下さい。
FullCalendarの使い方

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

誰でもわかる!オブジェクト指向(Rubyで解説)

オブジェクト指向とは?

オブジェクト指向とは「物」に注目した考え方のことです。
「物」には「属性(どんなもの)」と「操作(どのように動く)」という性質があります。
「物」がどんなもので、どのように動くかをあれこれ考えるのがオブジェクト指向です。

これだけではわかりづらいと思いますので、例を上げて説明します。

車で例えてみましょう。

車の「属性」

  • フェラーリ
  • 赤い
  • タイヤがある
  • ライトがある
  • ハンドルがある
  • バックミラーがある
  • エンジンがある

車の「操作」

  • 走る
  • 止まる
  • バックする
  • 曲がる
  • ドアが開く
  • ライトがつく
  • クラクションがなる
  • ウインカーが点滅する

など様々な「属性」や「操作」があります。
このような「物」を作るのには必ず「属性」と「操作」を組み合わせる必要があります。

プログラミングでは、このような「物」に注目して
クラス(設計図)やインスタンス(実体)を用いて作っていきます。

オブジェクトとは?

オブジェクトとは「物」です。
オブジェクト指向の説明でわかっているとは思いますが、「物」です。
プログラミングではクラス(設計図)とインスタンス(実体)を使って作っていきます。
クラスもインスタンスもオブジェクトという「物」です。
クラス(設計図)という「物」だし、
インスタンス(実体)という「物」です。

クラスとは?

# Carという名前のクラス(設計図)
class Car
  # 設計図の内容を記述していく
end

クラスとは「設計図」のことです。
何か物を作る時は最初に設計図を作ってから物を作り始めますよね。
〇〇クラスという枠の中にどんな物を作るかを書いていきます。
クラス名はこれから記述するコードの内容にあった名前がベストです。
クラス名の頭文字は必ず大文字で書きましょう。
クラスはあくまで設計図なので、このままではクラス内の事をWeb上に表示させたり、データベースに保存することはできません。

インスタンスとは?

# Carというインスタンスを生成している
Car.new

インスタンスとは「実体」のことです。
クラス名.newとすることで初めてクラスという「設計図」からインスタンスという「実体」を作り出す事ができます。
「実体」を作り出す事ができれば、それを表示させたり、データベースに保存する事ができます。

メンバ変数(クラス変数)とは?

# クラス変数nameに文字列のフェラーリを代入している
class Car
  @@name = "フェラーリ"
end

メンバ変数(クラス変数)とはオブジェクト指向における「属性」を定義したもの
オブジェクト指向では、「物」には「属性」と「操作」という性質があることを説明しました。
その中で「属性」(どんなもの)を定義するものがメンバ変数です。
JavaやC#にはメンバ変数があります。
しかしRubyにはメンバ変数がありませんが、似た変数としてクラス変数があります。
メンバ変数とクラス変数には細かい違いがいくつかありますが、ここでは割愛させていただきます。
Rubyのクラス変数は@@変数名で表します。

メソッドとは?

# Carクラスの中でrunメソッドを定義している
class Car
  def run
    puts '走るぜ!'
  end
end

メソッドとはオブジェクト指向における「操作」を定義したもの
オブジェクト指向では、「物」には「属性」と「操作」という性質があることを説明しました。
その中で「操作」(どのように動く)を定義するものがメソッドです。
Rubyではdef メソッド名で定義します。

実際にコードを書いてみよう

class Car
  @@name = "フェラーリ"
  def run
    puts "#{@@name}が走る!!"
  end
end

car = Car.new
puts car.run

とコードを書くと

フェラーリが走る!!

と表示されます。

まとめ

オブジェクト指向とは「物」に注目した考え方。
オブジェクトとは「物」。
オブジェクトには「属性」と「操作」という性質がある。
クラスとは「設計図」。
インスタンスとは「実体」。
クラスもインスタンスもオブジェクト。
メンバ変数(クラス変数)とはオブジェクト指向における「属性」を定義したもの。
メソッドとはオブジェクト指向における「操作」を定義したもの。

以上で終わりです。
曖昧なところや間違った考えをしている可能性もありますので、気になる事があれば是非コメントいただけたら幸いです。

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

Your Ruby version is 2.6.3, but your Gemfile specified 2.5.3の対処法

状況

ローカル開発環境でbundle installを実行しようとしたら以下のようなエラーが出ました。

terminal
Your Ruby version is 2.6.3, but your Gemfile specified 2.5.3

どうやら、開発環境で使われているrubyのバージョンと、Gemfileで指定しているrubyのバージョンが違うようです。

対処法

terminal
$ source ~/.bash_profile

このコマンドを実行すると、

ruby -v
ruby 2.5.3p105 

rubyのバージョンが切り替わりました。

rubyのバージョンを切り替えた後に、bundle installを実行すると、うまく通りました。

最後に

開発中に、いつの間にかrubyのバージョンが切り替わっていて、原因がわからないのですが、また何かわかったら記事にまとめます。

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

AWS EC2デプロイで困ったときに見たところ

はじめに

AWS EC2を使って自分のアプリケーションをデプロイするときに、非常に苦労をしたので、対処法をメモしておきます。

対処法

自分のアプリのディレクトリ直下に移動をしてから、logを出力します。

terminal
$ cd ~/var/www/rails/myapp # 自分のアプリがあるディレクトリに移動
$ cat /log/unicorn.log # logを出力

デプロイがうまくいかないときに、logファイルの中身を見るとエラーログが出ている時があるので、そのエラーログを見て対処していきましょう。

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

【feature】Rspecで作成したテストデータに引数を渡す方法 let! (){create()}

基本的なRspecの書き方についてはこちらに全て記載してあるので書きません。
https://qiita.com/jnchito/items/607f956263c38a5fec24

順序

・テストデータhoge_product、hoge_caetgoryを作成する
・hoge.categoryを持っているページにアクセスする
・カテゴリと紐づいた商品が表示されるテストを行う

コードに落とし込む

test_spec.rb
require 'rails_helper'

RSpec.feature "Categories", type: :feature do


let!(:hoge_caetgory) { create(:caetgory) } 
let!(:hoge_product) { create(:product ,caetgory: [caetgory]) } #caetgoryと言う引数(属性)を持ったproductを作成し、代入

  before do
    visit hoge_category_path(category)
  end

it カテゴリと紐づいた商品が表示される do
expect(page).to have_content hoge_product.category #caetgoryと言う引数(属性)を持ったhoge_productが表示されるテスト
end

書き方のみの説明記事のため、コントローラについては割愛。

間違いがありましたらコメントください。

関連記事はこちら。
https://qiita.com/kojiro3/items/b0957813695ca61fa3aa

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

【feature】Rspecで作成したテストデータに引数を渡す方法 let! ( ) {create( )}

基本的なRspecの書き方についてはこちらに全て記載してあるので書きません。
https://qiita.com/jnchito/items/607f956263c38a5fec24

順序

・テストデータhoge_product、hoge_caetgoryを作成する
・hoge.categoryを持っているページにアクセスする
・カテゴリと紐づいた商品が表示されるテストを行う

コードに落とし込む

test_spec.rb
require 'rails_helper'

RSpec.feature "Categories", type: :feature do


let!(:hoge_caetgory) { create(:caetgory) } 
let!(:hoge_product) { create(:product ,caetgory: [caetgory]) } #caetgoryと言う引数(属性)を持ったproductを作成し、代入

  before do
    visit hoge_category_path(category)
  end

it カテゴリと紐づいた商品が表示される do
expect(page).to have_content hoge_product.category #caetgoryと言う引数(属性)を持ったhoge_productが表示されるテスト
end

let!(:hoge_product) { create(:product ,caetgory: [caetgory1]) }だったら、
categoryがcategory1であるproductを作成してくれます。

let!(:変数) { create(:データ ,引数) }のイメージです。

書き方のみの説明記事のため、コントローラについては割愛。

間違いがありましたらコメントください。

関連記事はこちら。
https://qiita.com/kojiro3/items/b0957813695ca61fa3aa

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

【Vim】コメントを入力した時、カーソルが勝手に文頭に移動してしまう

問題

コメントを入力しようとしたら、勝手にカーソルが移動してインデントが消えてしまう問題です。

自分の場合はRubyファイルでしたが、

controller/items_controller.rb
    if @row.save
      redirect_to title_path(title_id), notice: "ITEM SAVED"
    else
      redirect_to title_path(title_id), notice: "SAVE FAILED"
    end

こういう場合、if文内にそれぞれ# 保存成功# 保存失敗を入れたくても、#を入力した瞬間にカーソルが移動してしまい、

controller/items_controller.rb
    if @row.save
# 保存成功
      redirect_to title_path(title_id), notice: "ITEM SAVED"
    else
      redirect_to title_path(title_id), notice: "SAVE FAILED"
    end

とズレてしまいました。
ああああああああああ!!!!!!!!!!!!!!!

原因

Vimの設定でfiletype plugin indent offになっていたからでした。

確認方法

Vimファイルを開いて
:filetype
で確認出来ます。

filetype detection:ON plugin:ON indent:ON

この3つすべてがONが良いみたいですね。

自分の場合は、3つ目のindentがOFFになっていました。

設定方法

読み込んでいる.vimrcで、(最後あたりに書く?)

.vimrc
filetype plugin indent on

これで解決しました。

雑記

最初はvim74/syntax/ruby.vimのあたりに原因があると思って、コメントを判別する行を消したりしてみました。
ただこのシンタックスは「このファイル形式だったら、こういうコードはこういう意味ですよ」という判別をしてくれてるファイルみたいですね。

問題はそのシンタックスの定義にもちゃんとインデント幅があり、それをちゃんと認識してなかったという事でした。

「vim ruby indent コメント」とかで検索しても、「コメント行の次の行までコメントアウトされてしまう!」問題ばかりがヒットして地味に見つけるのが大変だったのでまとめておきます。

以上です

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

駆け出す前に一歩立ち止まって、ユーザー登録機能におけるDB設計を考えよう!

記事内容

メルカリクローンサイトのユーザー登録機能を実装しました。
前回の記事でDB設計について記述しましたが、ユーザー機能の実装中にDB設計との変更点がありましたので、変更内容と変更理由を記載します。
駆け出しエンジニアは「とりあえず進みながら修正していこう!」とういうマインド」で開発がスタートすると思います。自分たちのチームもそうでした。
読んでくれた方がBD設計で手戻りがないようになればと思います。DB設計の修正が結構やっかいだったので...
前回の記事はこちら

この記事DB設計をした時点で考えていたこと。
ユーザー・アイテムモデルに大量のカラムができるから、括れるものは別テーブルを作成し1体1の関係でリレーションする。(テーブルにカラムが多すぎると不便になりそう・・・)

DB設計変更箇所

今回変更した箇所は以下の通りです。
変更しなければ実装できないというわけではないと思いますが、実装難易度(検索してヒットするか)、分かりやすさを考慮しました。

ユーザー/プロフィールテーブルのカラムを変更
生年月日のカラムを3分割(年・月・日)から1つに変更

ユーザー/プロフィールテーブルのカラムを変更

実際に修正したのはプロフィールテーブルに入れる予定だった、氏名、氏名(カナ)、生年月日をユーザーテーブルに移動。
今回ユーザー登録機能をウィザード形式で作成しました。(ウィザード形式について
ウィザード形式ではページを遷移する時sessionを用いて保存しますが、一つのページで登録するカラムが2つのテーブに別れていると登録できなかったため、1ページ目で入力したいカラムは1つのテーブル(users)にまとめました。
複数のテーブルへ保存する方法はあるようで、チャレンジしてみたのですが、deviseを使用するとできない??実装方法分かる方いましたら教えてください
[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する

変更前

users table

Column Type Options
nickname string null:false
password string null:false
email string null:false, unique: true, index:true

profiles table

Column Type Options
introduction text
avatar string
user references null: false, foreign_key: true
first_name string null:false
family_name string null:false
first_name_kana string null:false
family_name_kana string null:false
birth_year date null:false
birth_month date null:false
birth_day date null:false

変更後

users table

Column Type Options
nickname string null:false
password string null:false
email string null:false, unique: true, index:true
first_name string null:false
family_name string null:false
first_name_kana string null:false
family_name_kana string null:false
birth_year date null:false
birth_month date null:false
birth_day date null:false

profiles table

Column Type Options
introduction text
avatar string
user references null: false, foreign_key: true

生年月日のカラムを3分割(年・月・日)から1つに変更

続いてはuserモデルに移動した生年月日を登録する際、カラムを3つにわけていたものを1つにまとめたことです。これについては実装内容どうこうの話ではなく、単純に実装が早そうだったからです。
(【Rails】date_selectタグの使い方メモ

【結論】考えることと駆け出すことのバランスを!

走り始める前に「なぜそうするのか」理由を考える
考えた理由を元に走り始める
以上を繰り返して、知識を蓄積することが大切です。修正することも勉強になります!

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

【自分用】チョコプラのコント"カレー屋"の「入っちゃってる」かどうかを確かめるプログラムと、それを作って学んだこと

元ネタ

https://youtu.be/aVYQC9oDz88?t=173

コード

puts "長田「小辛は唐辛子マークでいうと何個分なの?」"
puts "松尾「"
mild_hot_min = gets
puts "から"
mild_hot_max = gets
puts "になります。」"
mild_hot_level = [*(mild_hot_min.to_i..mild_hot_max.to_i)]

puts "長田「じゃあ中辛は?」"
puts "松尾「"
normal_min = gets
puts "から"                                                   
normal_max = gets           
puts "になります。」"  
normal_hot_level = (normal_min.to_i..normal_max.to_i).to_a

haicchateru_yatsu = mild_hot_level & normal_hot_level
if haicchateru_yatsu.empty?
  puts "長田「・・・いや入っちゃってないんかい!」"
else
  puts "長田「いや#{(mild_hot_level & normal_hot_level).join('と')}が小辛にも中辛にも入っちゃってるじゃん。」"
  puts "松尾「・・・”入っちゃってる”というのは?(・。・) キョトン…」"
end

書いて学んだこと

RangeオブジェクトをArrayオブジェクトに変換する方法は、何個かある

mild_hot_level = [*(mild_hot_min.to_i..mild_hot_max.to_i)]

とか

normal_hot_level = (normal_min.to_i..normal_max.to_i).to_a

配列同士の積集合(どっちにもある要素)を求めたいなら、&でつなぐ

haicchateru_yatsu = mild_hot_level & normal_hot_level

他にも和集合や、もっと複雑なのも同じように求められます。
参考:https://qiita.com/namitop/items/be11c007da456ea95735

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

Rails 6 Grapeを利用したAPI作成、Swaggerでの確認

RoRでのAPI作成メモ 

ruby 2.7.0 & Rails 6
Rails 5.x 系の資源を利用する例です。
https://qiita.com/katafuchix/items/8a96e2fa9ddc8bc83545

プロジェクト作成

$ mkdir api_sample
$ cd api_sample
$ bundle init

Gemfile編集

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'rails'

インストール
*MySQLを利用する例

$ bundle install --path vendor/bundle
$ bundle exec rails new . --skip-action-mailer --skip-active-storage --skip-action-cable -d mysql

Gemfileに追加

# API
gem 'grape'
gem 'hashie-forbidden_attributes'
gem 'grape-jbuilder'
gem 'grape_on_rails_routes'
gem 'swagger-ui_rails'
gem 'grape-swagger'
gem 'grape-swagger-rails'

もう一度 bundle install を実行してエラーがないことを確認する

モデルクラス準備、データ投入

$ bin/rails g model Task name:string description:string

db/seed.rb

seed.rb
Task.create(name: 'タスク1', description: 'サンプルタスク1')
Task.create(name: 'タスク2', description: 'サンプルタスク2')
Task.create(name: 'タスク3', description: 'サンプルタスク3')
$ bundle exec rake db:create
$ bundle exec rake db:migrate
$ bundle exec rake db:seed

GrapeによるAPI作成

ルーティング

http://localhost:3000/api/v1/tasks/displays とかでアクセスしたいので各種設定
RailsかGrapeの仕様で「app/api」というディレトクリ以下のパス、ファイル名、クラス名が一致しなければならない。
それまでのRailsで「api/versions/v1」という作りをしている場合は、「api/api/versions/v1」というパスが二重になったフォルダ構成となってしまう。
ここでは以前のシステムを最小限度の手順で最新のRailsで組み直すため、ここはこのままで進めます。

config/routes.rb
Rails.application.routes.draw do
  mount Versions::V1::Api => '/'
  mount GrapeSwaggerRails::Engine => '/api/swagger'
end

APIディレクトリの作成

プロジェクト直下に api フォルダを作成してそこにAPIのプログラムを配置するために for Grape 以下の記述を追加

config/application.rb
require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module ApiSample
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    # For Grape
    config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
    config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
    config.middleware.use(Rack::Config) do |env|
      env['api.tilt.root'] = Rails.root.join 'app', 'views', 'api'
    end
  end
end
$ mkdir app/api
$ mkdir app/api/api
$ mkdir app/api/api/versions
$ mkdir app/api/api/versions/v1

API作成

大元にこういうのを作成してその中でエンドポイント単位のモジュールをincludeするのが管理しやすい
/api/vi/〜のパスにするために内部でこんな風に記述
テンプレートはJbuilderを指定

app/api/api/versions/v1/api.rb
module Versions
  module V1
    class Api < Grape::API
      version 'v1', using: :path
      format :json
      formatter :json, Grape::Formatter::Jbuilder
      prefix :api

      include ::Versions::V1::TaskDisplays

      # :nocov:
      if Rails.env.development? 
        add_swagger_documentation add_version: true
      end
      # :nocov:
    end
  end
end

各APIのコードはこんな感じ。 命名規則に注意。

app/api/api/versions/v1/task_displays.rb
module Versions
  module V1
    module TaskDisplays
      extend ActiveSupport::Concern
      included do
        namespace :tasks do
          namespace :displays do
            desc 'タスク一覧を取得する'
            get '', jbuilder: 'v1/task_displays/index' do
              @tasks = Task.all
            end
          end
        end
      end
    end
  end
end

テンプレードはこんな感じ

app/views/api/v1/task_displays/index.jbuilder
json.tasks @tasks do |task|
   json.(task, :id, :name, :description)
end

swagger設定ファイル

config/initializers/grape_swagger_rails.rb
unless Rails.env.production?
  GrapeSwaggerRails.options.app_name = 'Grape API Sample'
  GrapeSwaggerRails.options.app_url  = '/'
  GrapeSwaggerRails.options.url = 'api/v1/swagger_doc.json'
end

swagger用にjs, cssをimportするために2行追加

app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../stylesheets .css

// 下2行追加
//= link grape_swagger_rails/application.css
//= link grape_swagger_rails/application.js

確認

railsを起動してブラウザで確認

bin/rails s

API

http://localhost:3000/api/v1/tasks/displays
スクリーンショット 2020-03-15 2.02.32.png

swagger

http://localhost:3000/api/swagger
スクリーンショット 2020-03-15 2.02.15.png

あとは ここと同じように・・・
Rails 5.2 Grapeを利用したAPI作成、Swaggerでの確認 続編

サンプル

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

【随時更新】Railsでたまに見返したいメモ集

個人的に見返したい資料のメモ書きです。

HTTPメソッド一覧

HTTPメソッド 使用するリクエスト
GET ページを表示する操作のみを行う時
POST データを登録する操作をする時
PUT データを変更する操作をする時
DELETE データを削除する操作を行う時

アクション名一覧

アクション名 対応するリクエスト
index 一覧表示ページを表示
new 新規投稿ページを表示
create データの投稿
show 個別詳細ページを表示
update データの編集
destroy データの削除

createメソッド

メソッド 用途
all テーブルの全てのデータを取得する
find テーブルのレコードの内、ある1つのデータを取得する
create テーブルにレコードを追加する

例:

controller.rb
 def create
   Post.create(content: params[:content])
 end

content: テーブルのカラム名
params: paramsとして送られてきたデータ

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

Railsチュートリアルメモ - 第11章

(メモの目次記事はこちら)[https://qiita.com/yokohama4580/items/dedfd5510080273dc2a0]

(公式Railsチュートリアル第11章へのリンク)[https://railstutorial.jp/chapters/account_activation?version=5.1#cha-account_activation]

サマリ

  • メールの送信とアカウント有効化機能の実装
  • メタプログラミング
  • SendGridを利用した本番環境でのメール送信

ポイント

  • rails generate mailerでメーラーの雛形を作成できる
    • e.g. rails generate mailer UserMailer account_activation password_reset
    • 生成されるHTML/textメーラーのレイアウトはapp/views/layoutsで定義されている
  • コントローラー同様、メーラーの中で作成したインスタンス変数はテンプレートの中で使用することができる
  • 送信先やメールタイトルなどはmailメソッドで指定する
    • e.g. mail to:hoge@sample.com
  • deliver_nowメソッドを呼び出すことでメールを送信する
    • e.g. UserMailer.account_activation(@user).deliver_now
  • 名前付きroot
    • 名前付きrootの第一引数は:idになりBase64でエンコードされてURLが生成される
    • 第二引数としてハッシュを渡すとURLパラメーターにしてURLを生成してくれる。その際にエスケープも行ってくれる。
    • 名前付きrootの引数として指定されたものはコントローラー内でparams[:引数]でデコードされた状態で取り出すことができる
  • 以下を行うとメールのプレビューが有効になる
    • config/environments/development.rbに以下を追記する
    • test/mailers/previews/user_mailer_preview.rbを修正する
    • プレビューURLはhttps://localhost:3000/rails/mailers/user_mailer/
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = 'localhost:3000'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  • assert_matchで正規表現を利用してassertできる

  • sendメソッドは引数を評価してから実行するので、メタプログラミング(プログラムによるコードの生成)を行う際に利用される
    e.g.

 attribute = "foobar"
 user.send("#{attribute}_digest")
 # => user.foobar_digestが実行される
  • テストの中でのみassignsメソッドを使うと対応するアクション内のインスタンス変数にアクセスできるようになる

    • e.g. user = assigns(:user) => @userにアクセス可能になる
  • heroku addons:create sendgrid:starterでherokuにsendgridを導入できる

感想、詰まった箇所など

  • config/environments/development.rbを書き換えた後にサーバーの再起動を行わなかったせいでMissing host to link to! Please provide the :host parameter,...というエラーが出てしまい少し詰まった。
  • herokuにSendGridを追加した後、画面からユーザー登録を行うとメールが飛ばずにWe're sorry, but something went wrong.というエラー画面が表示された。ログを確認したところNet::SMTPAuthenticationError (535 Authentication failed: account disabledとなっていた。
  • 原因分からず解消しなかったため、herokuの画面からSendGridを「Delete Add-on」した後、再度SendGrid追加しようとしたところError Provisioning User - User status - bannedと表示されてしまった。
  • herokuアプリ自体を再作成してからsendgridを再追加することで解消した。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む