20210108のRailsに関する記事は19件です。

【Ruby】カラムの名前間違えた&制約間違えた〜助けてmigrate〜

解決したいこと

アプリケーションを作り始めて2日目。
DB設計を終えてテーブルを作り、モデルを作り、ふと気付く。

「あれ?スペル違ってるじゃないの・・・」
「あれ?null制約かかっててコンソールからデータ追加できないじゃないの・・・」

さてこれはどうしたものかと色々調べた結果、名前だけ変えられる素晴らしいコマンドがあるとのこと。
ありがたやありがたや・・・。

該当するソースコード

class CreateParties < ActiveRecord::Migration[6.0]
  def change
    create_table :parties do |t|
      t.string  :name,          null: false
      t.text    :iintroduction, null: false    #⬅️①なぜかiが多いスペルミス
      t.integer :season_id,     null: false
      t.integer :country_id,    null: false
      t.integer :genre_id,      null: false
      t.text    :picture,       null: false    #⬅️②これも修正したい
      t.timestamps
    end
  end
end

まずは①からターミナルで実行。

% rails generate migration rename_iintroduction_column_to_parties
                                 ⬆️変えたいカラム名          ⬆️モデル名

結果がこちら。
新しくrename用のマイグレーションファイルを作ってくれます。

Running via Spring preloader in process 9848
      invoke  active_record
      create    db/migrate/20210108122152_rename_iintroduction_column_to_parties.rb

次に作成してくれたファイルに記述していきます。

class RenameIintroductionColumnToParties < ActiveRecord::Migration[6.0]

  def change
    rename_column :parties, :iintroduction, :introduction
  end             ⬆️モデル名  ⬆️変えたいカラム名  ⬆️修正後のカラム名
end

記述できたらマイグレーション。

% rails db:migrate

直った!
次は②のnull:false制約をつけてしまったものを外したいという作業。
なぜかというとレビューサイトを作っているのですが、レビューしたいデータは管理者のみが作成できるようにしたいため、ひとまずデータの投稿をコンソールから行いたかったからです。
そこで画像データをコンソールから入力しようとしたところnull:false制約がついているためコンソールからデータ入力ができずに困っておりました。
一度この制約を外してとにかく画面上に一つでもデータが表示されるようにしたいというのもあったため、取り急ぎ外すことに。
こちらも同じようなコマンドで対応可能でした。

 % bin/rails g migration ChangeColumnToAllowNull

同じくマイグレーションファイルが作成されるのでそちらに記述。

class ChangeColumnToAllowNull < ActiveRecord::Migration[6.0]

  def up
    change_column_null :parties, :picture, null: true   #「up」でnull: trueに変更しますよ、という意味
  end

  def down
    change_column_null :parties, :picture, null: false  #「down」でnull: false制約つきのものから⬆️⬆️⬆️
  end

end

記述できたらマイグレーション。

% rails db:migrate

これで制約を外すことができたのでデータを追加することができました。
そして今、わたしの目の前にはいざ画像を追加しようと思ったら容量が大きすぎて追加できないというエラーが発生しております。
さあ次の戦場へ向かおう。

参考にさせて頂いた記事

https://qiita.com/libertyu/items/93acd8733e34b1d0a63c
https://qiita.com/mom0tomo/items/31466a80ca38db4ebf8c

ありがとうございました。

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

ActiveHash

ActiveHash の使い方 まとめ

Active_Hashとは、、、
都道府県名などの変更されないデータを「モデルファイル内」に直接記述することで、
データベースへ保存せずにデータを取り扱うことができる Gem のこと。

ActiveHash導入方法

Gemfileを編集

Gemfile
gem 'active_hash'

記述したらbundle installを実行

例 ArticleモデルでActiveHashを導入

記事を管理するArticleモデル
記事のジャンルを管理するGenreモデル
記事のジャンルは変更されないデータ=ActiveHashを用いて管理

①それぞれのモデルを作成
rails g model article
rails g model genre --skip-migration

--skip-migrationとは??
モデルファイルを作成するときに、マイグレーションファイルの生成を行わないためのオプション。今回、記事のジャンルの情報はデータベースに保存しない=マイグレーションファイルを作成する必要はない。

②genreモデルでクラスを定義し、ActiveHash::Base を継承するための記述を行う

ジャンルのデータは、配列にハッシュ形式で格納

models/genre.rb
class Genre < ActiveHash::Base
 self.data = [
   { id: 1, name: '--' },
   { id: 2, name: '経済' },
   { id: 3, name: '政治' },
   { id: 4, name: '地域' },
   { id: 5, name: '国際' },
   { id: 6, name: 'IT' },
   { id: 7, name: 'エンタメ' },
   { id: 8, name: 'スポーツ' },
   { id: 9, name: 'グルメ' },
   { id: 10, name: 'その他' }
 ]
 end
③①で生成されたarticleのマイグレーションファイルを編集・マイグレートする

以下の様に編集し ⇨  rails db:migrate
Articlesテーブルの中にgenre_idという名前のカラムを作成しているのは、投稿した記事を表示する際に、その記事に紐付いたジャンルを取得するため
Articleテーブルの中で、Genreモデル(ActiveHash)のidを外部キーとして管理することで、その記事に紐付いたジャンルの取得が実現

db/migrate/20XXXXXXXXXXXX_create_articles.rb
class CreateArticles < ActiveRecord::Migration[6.0]
 def change
   create_table :articles do |t|
     t.string     :title        , null: false
     t.text       :text         , null: false
     t.integer    :genre_id     , null: false
     t.timestamps
   end
 end
end
④モデル間でのアソシエーションの設定

ActiveHashを用いてアソシエーションを設定する場合は、ActiveHashで定義されているmoduleをモデルに取り込む必要がある。

1)ActiveHashを導入したい(される)モデル
投稿する記事=Articleは、1つのジャンル=Genreに紐付いています。そのため、Articleモデルにbelongs_toを設定します。
ActiveHashを用いて、belongs_toを設定するには、
extend ActiveHash::Associations::ActiveRecordExtensionsと記述してmoduleを取り込みます。

app/models/article.rb
class Article < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to :genre
end

2)ActiveHashを設定したモデル
1つのジャンル=Genreは、たくさんの投稿物=Articlesに紐付いています。そのため、Genreモデルにはhas_manyを設定します。

ActiveHashを用いて、has_manyを設定するには、
include ActiveHash::Associationsと記述してmoduleを取り込みます。

app/models/genre.rb
class Genre < ActiveHash::Base
 self.data = [
   { id: 1, name: '--' },
  〜〜〜省略〜〜〜
   { id: 10, name: 'その他' }
 ]

  include ActiveHash::Associations
  has_many :articles

 end

※moduleとは、特定の役割を持つメソッドや定数に名前を付けてまとめたもの。どのようなmoduleが定義されているかは、こちらのリファレンスで確認。

※ActiveHashを用いたアソシエーションの設定は、他にもあります。詳しくはこちらのドキュメントを確認。

⑤バリデーションを設定

データベースに空の投稿が保存されないようにする場合、バリデーションヘルパーのnumericality(数値かどうかを検証する)を用いる。数値であればデータベースに保存を許可して、それ以外では保存が許可されないようにできます。今回においては、--を保存されないようにしたいので、id: 1以外であれば保存できるように設定すると

app/models/article.rb
class Article < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to :genre

  #空の投稿を保存できないようにする
  validates :title, :text, presence: true

  #ジャンルの選択が「--」の時は保存できないようにする
  validates :genre_id, numericality: { other_than: 1 } 
end

このバリデーションは、genre_idのid:1以外のときに保存できるという意味になる。

導入については以上です。ジャンル選択のプルダウン生成などはview関連になるため
別記事に追記予定。

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

エラーメッセージを日本語に変換 複数テーブル対応

前提

エラーメッセージを日本語にする際、ネストしたモデルを日本語変換する際に苦戦したため備忘録として書きます。

親テーブル

recipes
id
title
description

子テーブル

ingredients
id
name
amount
recipe_id(FK)

参考サイト

https://qiita.com/satreu16/items/a072a4be415f30087ed7
https://blog.cloud-acct.com/posts/u-rails-error-messages-jayml/
https://qiita.com/Ushinji/items/242bfba84df7a5a67d5b

方法

railsを日本語化するgemです。

Gemfile
gem 'rails-i18n'

bundleします。
下記の二行を追記します。

config/application.rb
config.load_defaults 6.0

#追記
config.i18n.default_locale = :ja
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.yml').to_s]

子テーブルはattributesに複数形で記述します。

config/locales/models/ja.yml
ja:
  activerecord:
    models:
      recipe: レシピ
    attributes:
      recipe:
        title: 料理名
        description: コメント
      ingredients:
        name: 材料
        amount: 

これで日本語に対応しているはずなので、フォームで確認しましょう。

deviseを日本語化

config/locales/devise.ja.ymlを作成し、下記の内容を貼り付けます。

config/locales/devise.ja.yml
ja:
  devise:
    confirmations:
      confirmed: 'アカウントを登録しました。'
      send_instructions: 'アカウントの有効化について数分以内にメールでご連絡します。'
      send_paranoid_instructions: "あなたのメールアドレスが登録済みの場合、本人確認用のメールが数分以内に送信されます。"
    failure:
      already_authenticated: 'すでにログインしています。'
      inactive: 'アカウントが有効化されていません。メールに記載された手順にしたがって、アカウントを有効化してください。'
      invalid: "%{authentication_keys} もしくはパスワードが不正です。"
      locked: 'あなたのアカウントは凍結されています。'
      last_attempt: 'あなたのアカウントが凍結される前に、複数回の操作がおこなわれています。'
      not_found_in_database: "%{authentication_keys} もしくはパスワードが不正です。"
      timeout: 'セッションがタイムアウトしました。もう一度ログインしてください。'
      unauthenticated: 'アカウント登録もしくはログインしてください。'
      unconfirmed: 'メールアドレスの本人確認が必要です。'
    mailer:
      confirmation_instructions:
        subject: 'アカウントの有効化について'
      reset_password_instructions:
        subject: 'パスワードの再設定について'
      unlock_instructions:
        subject: 'アカウントの凍結解除について'
      password_change:
        subject: 'パスワードの変更について'
    omniauth_callbacks:
      failure: "%{kind} アカウントによる認証に失敗しました。理由:(%{reason})"
      success: "%{kind} アカウントによる認証に成功しました。"
    passwords:
      no_token: "このページにはアクセスできません。パスワード再設定メールのリンクからアクセスされた場合には、URL をご確認ください。"
      send_instructions: 'パスワードの再設定について数分以内にメールでご連絡いたします。'
      send_paranoid_instructions: "あなたのメールアドレスが登録済みの場合、パスワード再設定用のメールが数分以内に送信されます。"
      updated: 'パスワードが正しく変更されました。'
      updated_not_active: 'パスワードが正しく変更されました。'
    registrations:
      destroyed: 'アカウントを削除しました。またのご利用をお待ちしております。'
      signed_up: 'アカウント登録が完了しました。'
      signed_up_but_inactive: 'ログインするためには、アカウントを有効化してください。'
      signed_up_but_locked: 'アカウントが凍結されているためログインできません。'
      signed_up_but_unconfirmed: '本人確認用のメールを送信しました。メール内のリンクからアカウントを有効化させてください。'
      update_needs_confirmation: 'アカウント情報を変更しました。変更されたメールアドレスの本人確認のため、本人確認用メールより確認処理をおこなってください。'
      updated: 'アカウント情報を変更しました。'
    sessions:
      signed_in: 'ログインしました。'
      signed_out: 'ログアウトしました。'
      already_signed_out: '既にログアウト済みです。'
    unlocks:
      send_instructions: 'アカウントの凍結解除方法を数分以内にメールでご連絡します。'
      send_paranoid_instructions: 'アカウントが見つかった場合、アカウントの凍結解除方法を数分以内にメールでご連絡します。'
      unlocked: 'アカウントを凍結解除しました。'
  errors:
    messages:
      already_confirmed: 'は既に登録済みです。ログインしてください。'
      confirmation_period_expired: "の期限が切れました。%{period} までに確認する必要があります。 新しくリクエストしてください。"
      expired: 'の有効期限が切れました。新しくリクエストしてください。'
      not_found: 'は見つかりませんでした。'
      not_locked: 'は凍結されていません。'
      not_saved:
        one: "エラーが発生したため %{resource} は保存されませんでした:"
        other: "%{count} 件のエラーが発生したため %{resource} は保存されませんでした:"
      taken: "は既に使用されています。"
      blank: "が入力されていません。"
      too_short: "は%{count}文字以上に設定して下さい。"
      too_long: "は%{count}文字以下に設定して下さい。"
      invalid: "は有効でありません。"
      confirmation: "が内容とあっていません。"

Userモデルを追記します。

config/locales/models/ja.yml
ja:
  activerecord:
    models:
      recipe: レシピ
      user: ユーザー
    attributes:
      recipe:
        title: 料理名
        description: コメント
      ingredients:
        name: 材料
        amount: 量
      user:
        name: ユーザー名
        email: メールアドレス
        password: パスワード
        password_confirmation: 確認用パスワード
        remember_me: 次回から自動的にログイン

ユーザ名は人によってカラム名がnameでない可能性があるので注意。
これで完成です。

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

#楽天APIのデータをテーブルに格納する方法

はじめに

ポートフォリオ等でwebアプリを開発していると、「外部APIを利用してみたい」という方もいるかと思います。

今回紹介する楽天APIに関しては、データを取得すること自体は、そんなに難しくありません。
アプリIDを取得してgemをインストールすれば、割と簡単にデータを取得することができます。

ただし、「APIのデータをテーブルに格納して他のテーブルと関連付けて…」というように、取得したデータをアプリ内で活用しようとするとやや難易度が上がります(個人的な考えですが笑)

本記事では「取得したデータをテーブルに格納する方法」と「アソシエーションの設定」について記載していきます。
また、最後にアプリ内で検索機能を設けて、必要なデータを表示させるコードも簡単に記載しました。

これから楽天APIを使ってみたいという方の参考になれば幸いです。

注意

  • 本記事は楽天APIについて言及しております。
    また楽天APIにも様々ありますが、今回は楽天ブックス書籍検索APIを用います。

  • 本記事では、APIのデータ取得の部分(アプリIDの取得とgemのインストール)は割愛します。
    データ取得部分については、以下の記事を参考にしてみてください。
     
    https://freesworder.net/rakuten-api-rails/
    https://qiita.com/hakusai_it/items/6453c4577647cb8995d3

環境

  • Ruby version 2.7.2
  • Rails version 6.0.3.4

ER図

今回は以下のER図にて、話を進めていきます。
Bookテーブルがデータを格納するテーブルです。
取得した本について、レビューを記載するために、Reviewテーブルを設けています。

ER図.png

Bookテーブルのカラムについて少し説明します。
今回Bookテーブルのprimary_keyは『id』ではなく、商品の固有の番号である『isbn』を使っていきます。
『title』,『author』はそれぞれ、本のタイトルと著者名です。
『item_caption』は商品の説明、『item_url』は楽天の商品のurl、『middleimage_url』は本の画像です。
その他にも様々なデータがありますので、気になる方は以下のURLを参考にしてください。
https://webservice.rakuten.co.jp/api/booksbooksearch/

実装工程

概要

以下のような流れで実装していきます。

step1. テーブルの作成
step2. アソシエーションの設定
step3. ルーティングの設定
step4. コントローラーの設定
step5. 検索ページの作成

「アソシエーションの設定」はstep1・step2、
「取得したデータをテーブルに格納」はstep3・step4、
「検索ページの実装」はstep5で実装します

step1. テーブルの作成

各テーブルを作成していきます。
前述の通り、今回は User, Book, Reviewテーブルを作っていきます。

Userテーブル作成

Userテーブルは特に変わったことはしません。
モデルを作成して、マイグレーションを実行していきましょう!

$ rails g model User name:string email:string password_digest:string
$ rails g db:migrate

Bookテーブル作成

まずはモデルを作成していきます。

$ rails g model Book title:string author:string isbn:bigint url:string image_url:string

次にmigrationファイルを書き換えていきます。
Bookテーブルのprimary_keyは『id』ではなく、商品の固有の番号である『isbn』を使っていくため、ファイルの書き換えが必要になります。

ファイル名の米印にはMigration ID(日付等が書いてある数字)が入ります。
ActiveRecord::Migration[6.0]の部分は人によって異なると思います。

isbn部分にnull: false, primary_key: trueを追記します。

**************_create_books.rb
class CreateBooks < ActiveRecord::Migration[6.0]
  def change
    create_table :books, id: false do |t|
      t.string :title
      t.string :author
      t.bigint :isbn, null: false, primary_key: true
      t.string :url
      t.string :image_url

      t.timestamps
    end
  end
end

マイグレーションファイルを書き換えたらマイグレーションしていきます。

$ rails g db:migrate

Reviewテーブル作成

最後にReviewテーブルを作成していきます。

$ rails g model Review content:string user:references book:references

次にmigrationファイルを書き換えていきます。
ファイル名の米印にはMigration ID(日付等が書いてある数字)が入ります。
ActiveRecord::Migration[6.0]の部分は人によって異なると思います。

**************_create_reviews.rb
class CreateReviews < ActiveRecord::Migration[6.0]
  def change
    create_table :books, id: false do |t|
      #bookの部分に記載してあったforeign_key: trueを削除する
      t.references :book, null: false
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
    #この部分の新たに以下のコードを記載
    add_foreign_key :bookcases, :books, column: :book_id , primary_key: :isbn
  end
end

マイグレーションファイルを書き換えたらマイグレーションを実行していきます。

$ rails g db:migrate

これでテーブルの作成は以上です。
次はアソシエーションの設定です。

step2. アソシエーションの設定

各model.rbのアソシエーションを設定してきます。
ここでもBookテーブルのprimary_keyを『isbn』になるようコードを書いていきます。

user.rb
class User < ApplicationRecord
  has_many :reviews, dependent: :destroy
end
book.rb
class Book < ApplicationRecord
  self.primary_key = "isbn"
  has_many :reviews, dependent: :destroy
end

review.rb
class Bookcase < ApplicationRecord
  belongs_to :user
  belongs_to :book, primary_key: "isbn"
end

step2までで、テーブル作成とアソシエーションの設定は終了です。
step3以降はBookテーブルにデータを格納する方法を主に説明していきますので、User, Reviewモデルについては割愛し、Bookモデルについてのみ記載していきます。

step3. ルーティングの設定

今回は検索欄と検索結果を表示するために/searchアクションを設けています。
必要であれば、ご自身で追加のアクションを設定してください。

routes.rb
get 'books/search', to: "books#search"

step4. コントローラの設定

まずはコントローラファイルを作成していきます。

$ rails g controller books

作成したコントローラファイルに以下のコードを記載していきます。

books_controller.rb
class BooksController < ApplicationController

  def search
    #ここで空の配列を作ります
    @books = []
    @title = params[:title]
    if @title.present?
      #この部分でresultsに楽天APIから取得したデータjsonデータを格納します
      #今回は書籍のタイトルを検索して一致するデータを格納するように設定しています
      results = RakutenWebService::Books::Book.search({
        title: @title,
      })
      #この部分で@booksにAPIからの取得したJSONデータを格納していきます
      #read(result)についてはprivateメソッドとして設定しております
      results.each do |result|
        book = Book.new(read(result))
        @books << book
      end
    end
    #「@books内の各データをそれぞれ保存していきます
    #すでに保存済の本は除外するためにunlessの構文を記載しています
    @books.each do |book|
      unless Book.all.include?(book)
        book.save
      end
    end
  end

  private
  #「楽天APIのデータから必要なデータを絞り込む」、且つ対応するカラムにデータを格納するメソッドを設定していきます
  def read(result)
    title = result["title"]
    author = result["author"]
    url = result["itemUrl"]
    isbn = result["isbn"]
    image_url = result["mediumImageUrl"].gsub('?_ex=120x120', '')
    book_genre_id = result["booksGenreId"]
    item_caption = result["itemCaption"]
    {
      title: title,
      author: author,
      url: url,
      isbn: isbn,
      image_url: image_url,
      book_genre_id: book_genre_id,
      item_caption: item_caption
    }
  end
end

step4までで、テーブルへのデータ格納は実装完了です。
step5では検索ページと結果の出力ページを作成していきます。

step5. 検索ページの作成

search.html.erbというファイルを作成し、コードを書いていきます。
※本記事では最低限のコードのみ記載しております。適宜classを設定し、見た目を改善しましょう!

search.html.erb
#検索バーを表示
<%= form_tag(books_search_path, method: :get) do %>
  <%= text_field_tag :title, @title %>
  <%= button_tag type: "submit" %>
<% end %>

#検索結果を表示
<% if @books %>
  <% @books.each do |book| %>
  #ご自身が表示させたいデータを記載してください
  #以下のコードではは画像タイトル著者名商品の説明を表示させています
    <%= image_tag book.image_url %>
    <%= book.title %>
    <%= book.author %>
    <%= book.item_caption %>    
  <% end %>
<% end %>

以上で実装工程は終了となります。
何かご不明点や誤っている点がございましたら、コメントにて教えていただけると幸いです。

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

楽天APIのデータをテーブルに格納する方法

はじめに

ポートフォリオ等でwebアプリを開発していると、「外部APIを利用してみたい」という方もいるかと思います。

今回紹介する楽天APIに関しては、データを取得すること自体は、そんなに難しくありません。
アプリIDを取得してgemをインストールすれば、割と簡単にデータを取得することができます。

ただし、「APIのデータをテーブルに格納して他のテーブルと関連付けて…」というように、取得したデータをアプリ内で活用しようとするとやや難易度が上がります(個人的な考えですが笑)

本記事では「取得したデータをテーブルに格納する方法」と「アソシエーションの設定」について記載していきます。
また、最後にアプリ内で検索機能を設けて、必要なデータを表示させるコードも簡単に記載しました。

これから楽天APIを使ってみたいという方の参考になれば幸いです。

注意

  • 本記事は楽天APIについて言及しております。
    また楽天APIにも様々ありますが、今回は楽天ブックス書籍検索APIを用います。

  • 本記事では、APIのデータ取得の部分(アプリIDの取得とgemのインストール)は割愛します。
    データ取得部分については、以下の記事を参考にしてみてください。
     
    https://freesworder.net/rakuten-api-rails/
    https://qiita.com/hakusai_it/items/6453c4577647cb8995d3

環境

  • Ruby version 2.7.2
  • Rails version 6.0.3.4

ER図

今回は以下のER図にて、話を進めていきます。
Bookテーブルがデータを格納するテーブルです。
取得した本について、レビューを記載するために、Reviewテーブルを設けています。

ER図.png

Bookテーブルのカラムについて少し説明します。
今回Bookテーブルのprimary_keyは『id』ではなく、商品の固有の番号である『isbn』を使っていきます。
『title』,『author』はそれぞれ、本のタイトルと著者名です。
『item_caption』は商品の説明、『item_url』は楽天の商品のurl、『middleimage_url』は本の画像です。
その他にも様々なデータがありますので、気になる方は以下のURLを参考にしてください。
https://webservice.rakuten.co.jp/api/booksbooksearch/

実装工程

概要

以下のような流れで実装していきます。

step1. テーブルの作成
step2. アソシエーションの設定
step3. ルーティングの設定
step4. コントローラーの設定
step5. 検索ページの作成

「アソシエーションの設定」はstep1・step2、
「取得したデータをテーブルに格納」はstep3・step4、
「検索ページの実装」はstep5で実装します

step1. テーブルの作成

各テーブルを作成していきます。
前述の通り、今回は User, Book, Reviewテーブルを作っていきます。

Userテーブル作成

Userテーブルは特に変わったことはしません。
モデルを作成して、マイグレーションを実行していきましょう!

$ rails g model User name:string email:string password_digest:string
$ rails g db:migrate

Bookテーブル作成

まずはモデルを作成していきます。

$ rails g model Book title:string author:string isbn:bigint url:string image_url:string

次にmigrationファイルを書き換えていきます。
Bookテーブルのprimary_keyは『id』ではなく、商品の固有の番号である『isbn』を使っていくため、ファイルの書き換えが必要になります。

ファイル名の米印にはMigration ID(日付等が書いてある数字)が入ります。
ActiveRecord::Migration[6.0]の部分は人によって異なると思います。

isbn部分にnull: false, primary_key: trueを追記します。

**************_create_books.rb
class CreateBooks < ActiveRecord::Migration[6.0]
  def change
    create_table :books, id: false do |t|
      t.string :title
      t.string :author
      t.bigint :isbn, null: false, primary_key: true
      t.string :url
      t.string :image_url

      t.timestamps
    end
  end
end

マイグレーションファイルを書き換えたらマイグレーションしていきます。

$ rails g db:migrate

Reviewテーブル作成

最後にReviewテーブルを作成していきます。

$ rails g model Review content:string user:references book:references

次にmigrationファイルを書き換えていきます。
ファイル名の米印にはMigration ID(日付等が書いてある数字)が入ります。
ActiveRecord::Migration[6.0]の部分は人によって異なると思います。

**************_create_reviews.rb
class CreateReviews < ActiveRecord::Migration[6.0]
  def change
    create_table :books, id: false do |t|
      #bookの部分に記載してあったforeign_key: trueを削除する
      t.references :book, null: false
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
    #この部分の新たに以下のコードを記載
    add_foreign_key :bookcases, :books, column: :book_id , primary_key: :isbn
  end
end

マイグレーションファイルを書き換えたらマイグレーションを実行していきます。

$ rails g db:migrate

これでテーブルの作成は以上です。
次はアソシエーションの設定です。

step2. アソシエーションの設定

各model.rbのアソシエーションを設定してきます。
ここでもBookテーブルのprimary_keyを『isbn』になるようコードを書いていきます。

user.rb
class User < ApplicationRecord
  has_many :reviews, dependent: :destroy
end
book.rb
class Book < ApplicationRecord
  self.primary_key = "isbn"
  has_many :reviews, dependent: :destroy
end

review.rb
class Bookcase < ApplicationRecord
  belongs_to :user
  belongs_to :book, primary_key: "isbn"
end

step2までで、テーブル作成とアソシエーションの設定は終了です。
step3以降はBookテーブルにデータを格納する方法を主に説明していきますので、User, Reviewモデルについては割愛し、Bookモデルについてのみ記載していきます。

step3. ルーティングの設定

今回は検索欄と検索結果を表示するために/searchアクションを設けています。
必要であれば、ご自身で追加のアクションを設定してください。

routes.rb
get 'books/search', to: "books#search"

step4. コントローラの設定

まずはコントローラファイルを作成していきます。

$ rails g controller books

作成したコントローラファイルに以下のコードを記載していきます。

books_controller.rb
class BooksController < ApplicationController

  def search
    #ここで空の配列を作ります
    @books = []
    @title = params[:title]
    if @title.present?
      #この部分でresultsに楽天APIから取得したデータjsonデータを格納します
      #今回は書籍のタイトルを検索して一致するデータを格納するように設定しています
      results = RakutenWebService::Books::Book.search({
        title: @title,
      })
      #この部分で@booksにAPIからの取得したJSONデータを格納していきます
      #read(result)についてはprivateメソッドとして設定しております
      results.each do |result|
        book = Book.new(read(result))
        @books << book
      end
    end
    #「@books内の各データをそれぞれ保存していきます
    #すでに保存済の本は除外するためにunlessの構文を記載しています
    @books.each do |book|
      unless Book.all.include?(book)
        book.save
      end
    end
  end

  private
  #「楽天APIのデータから必要なデータを絞り込む」、且つ対応するカラムにデータを格納するメソッドを設定していきます
  def read(result)
    title = result["title"]
    author = result["author"]
    url = result["itemUrl"]
    isbn = result["isbn"]
    image_url = result["mediumImageUrl"].gsub('?_ex=120x120', '')
    book_genre_id = result["booksGenreId"]
    item_caption = result["itemCaption"]
    {
      title: title,
      author: author,
      url: url,
      isbn: isbn,
      image_url: image_url,
      book_genre_id: book_genre_id,
      item_caption: item_caption
    }
  end
end

step4までで、テーブルへのデータ格納は実装完了です。
step5では検索ページと結果の出力ページを作成していきます。

step5. 検索ページの作成

search.html.erbというファイルを作成し、コードを書いていきます。
※本記事では最低限のコードのみ記載しております。適宜classを設定し、見た目を改善しましょう!

search.html.erb
#検索バーを表示
<%= form_tag(books_search_path, method: :get) do %>
  <%= text_field_tag :title, @title %>
  <%= button_tag type: "submit" %>
<% end %>

#検索結果を表示
<% if @books %>
  <% @books.each do |book| %>
  #ご自身が表示させたいデータを記載してください
  #以下のコードではは画像タイトル著者名商品の説明を表示させています
    <%= image_tag book.image_url %>
    <%= book.title %>
    <%= book.author %>
    <%= book.item_caption %>    
  <% end %>
<% end %>

以上で実装工程は終了となります。
何かご不明点や誤っている点がございましたら、コメントにて教えていただけると幸いです。

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

【Ruby On Rails】update_columnを使って、計算した結果をinteger型のカラムへ更新する方法

備忘録です。

updateとupdate_columnについて

テーブル内の情報を更新する際に、レコードを更新したい場合はupdateメソッドを使います。
しかし、特定のカラムだけを更新したい場合は、updateは使えません。
そこで、update_columnを使用します。

使用例

前提として、usersテーブル:post_countという投稿回数をカウントするinteger型のカラムがあることとします。

ユーザーが投稿する度に、投稿回数(=post_count)が加算されていくものは以下の通りです。

    sum = current_user.post_count.to_i + 1
    current_user.update_column(:post_count, sum.to_i)

初期値がnilの場合もしっかりと足し算ができるように、to_iを付けています。
カラムがnilの状態で、to_iを付けずに実行すると以下のようなエラーが出ます。

undefined method `+' for nil:NilClass
スクリーンショット 2021-01-08 19.27.43.png

to_iをしてあげることで、nilを0という数字として認識させることができ、計算ができます。

参考記事

https://qiita.com/lemtosh469/items/371544fa4fd3c333adf1
https://teratail.com/questions/19963

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

【Ruby on Rails】Rails tutorial 14章 ステータスフィードの実装方法まとめ

はじめに

Rails tutorialに出てくるステータスフィードの実装が少しややこしかったので自分なりにまとめておきます。

ステータスフィード

ステータスフィードとは、ツイッターなどでいうTL(tweet list)のことです。
フォローしているユーザーの投稿を表示することが可能です。

実装方法

feedメソッドを作成します。

user.rb
#ステータスのフィードを返す。
def feed
end

はじめに結論から描きます。feedメソッドには以下のように記載します。

user.rb
#ステータスのフィードを返す。
  def feed
    following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)
  end

これだけ見てもワケワカリマセン。詳しく詳細を見ていきます。
まずは以下に着目します。

user.rb
    following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"

上記のコードは、SQL文で表されていてSELECTコマンドが使われています。

SQLコマンド 意味
SELECT テーブルのデータを検索します。
SELECTコマンドのパラメータ 意味
FROM 対象となるソーステーブルを指定します。
WHERE 取得したい値の条件を設定する

つまり、ここで何を意味しているかというと、、、
relationshipsテーブルのfollowed_idカラムがuser_idと一致しているユーザーを取得すると言う意味になります。following_ids変数フォローしているユーザー情報を取得することができます。

次に以下コードに着目します。

user.rb
Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)

これはrailsのwhereメソッドを使っています。
whereメソッドでも使い方が少しややこしかったので整理していきます。

まずはINORを見ていきます。
IN複数の条件を定義するために使います。
以下に例を記載します。

#単体指定
#ageカラムが「20」のユーザーを取得します。
user = User.where("age = 20")

#複数指定
#ageカラムが「20と30」のユーザーを取得します。
user = User.where("age IN (20, 30)")

上のコードに戻ってみ考えてみると、、、
user_idの値が、先ほど定義したfollowing_ids(フォローしているユーザー一覧)のidの値の投稿を取得するということになります。

user.rb
Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)

次にORに着目していきます。
ORどちらかの条件に一致するデータを取得するという意味です。
以下に例を記載します。

#nameカラムが「太郎」でageカラムが「20」のユーザーを取得します。
user = User.where("name = '太郎' and age = 20")

こちらも上のコードに戻って考えてみると、、、
user_idの値がfollowing_ids(フォローしているユーザー一覧)のidもしくは"id"(自分のid)であればその値を返すということになります。

user.rb
Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id)

ちなみにuser_id = :user_id", user_id: idの部分については、:で指定されている値が,後に指定されている値に代入されるというような挙動になっています。

#ageカラムが「20」のユーザーを取得します。
user = User.where("age = :xxx", xxx: 20)

#ageカラムが「20」のユーザーを取得します。
user = User.where("age = 20")

feedメソッドの理解はできました。
feedメソッドを以下のように使うと、ログインしているユーザーがフォローしているユーザーの投稿を取得することができます。

current_user.feed

参考文献

Rails tutorial 第14章 ユーザーをフォローする
https://.jp/chapters/following_users?version=6.0#sec-the_status_feed

Pikawaka 【Rails】whereメソッドを使って欲しいデータの取得をしよう!
https://pikawaka.com/rails/where

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

rails newをしたらPG::ConnectionBad: could not connect to server: No such file or directoryとエラーが出た

「PostgreSQLが起動していないよ」というエラーのようです。
PCの再起動によるものと思われますが、以下の方法で解決できました。
何度も遭遇している割には、復旧手順を毎回調べていると感じたので記録しておきます。

手順

①PostgreSQLが出力するログファイルの前まで行く

$ cd /usr/local/var/log

②ファイルの内容を確認

$ cat postgres.log

   ↓↓↓
スクリーンショット 2020-12-27 16.19.25.png
lock file "postmaster.pid" already existsとたくさん表示されました。
postmaster.pidファイルが既にあるとのことなので削除しました。

④rmコマンドで当該ファイルを削除

$ rm /usr/local/var/postgres/postmaster.pid

削除後、無事にrails newを実行することができました。

結果

postmaster.pidは、サーバーが複数起動されるのを防止するための仕組みで、サーバーの起動と共に作成され、停止と同時に削除されるようです。
サーバーが正常に停止されないとファイルが残ってしまうことがあり、今回のようなエラーに繋がるという事ですね。

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

Vue.js + Rails で電子年賀状アプリを作った話

背景

自分は季節の行事や習慣が好きでそれを自分の得意なことで表現したいということから年賀状のWebサイトを作ろうと考えました。当初は自分がWebサイトを作ってそれを年賀状として近しい人に見てもらおうと考えていましたが、年賀状を作れるWebアプリの方が面白いなと考え、誰でも年賀状を作成できるアプリに変更しました。

PCユーザーをメインで作成しました。
しかし、大半の人がスマホで遊んでくれたことでスマホのバグがたくさん見つかり、年末はバグと戦いましたが、今ではスマホでも難なく遊べるはずです。。

使用技術

  • Vue.js・・・フロントエンド
  • Rails・・・バックエンド
  • FirebaseAuth・・・ログイン認証
  • TwitterAPI・・・年賀状の公開範囲の設定

↓↓↓デプロイも済ませておりますので、ぜひ遊んでみて下さい↓↓↓
https://newyearmaker.netlify.app/card/new

開発環境

  • macOS 10.15.7
  • Ruby 2.7.2
  • Rails 6.0.3
  • vue/cli 4.5.6

軽くデモ

1. まずは年賀状作成ページから

私のビデオ2 (1).gif

  • 年賀状の動く背景を選択
  • 年賀状に乗せるメッセージの入力
  • 年賀状の公開範囲の設定

2. 次に年賀状ページ

私のビデオ6.gif

  • anime.jsによるアニメーション
  • 限定公開の場合ページを読み込む際に見ているユーザー情報から公開制限を行っている
  • 左下のボタンから年賀状の受け取り
  • 右下のボタンから年賀状作成ページへ

3. 最後にユーザーページ

私のビデオ2.gif

  • 年賀状ページから受け取った年賀状一覧を表示、クリックすることで年賀状を確認することが可能
  • 自分が作った年賀状の編集ページへ

工夫したところ

誰でも気軽に使ってもらいたかったので、ログインしなくても遊べるようにしました(ログインしない場合は機能が制限されてしまいますが)。
自分の周りでTwitterを使っている人が多かったので、フォロワー限定公開などの機能を実装しました。firebaseを用いてTwitterでのログインを可能にしており、実装がとても簡単でした。
ユーザーページは12/31にひらめいて必死に作ってたらガキ使が終わってました。。。(笑)

感想

Railsはチュートリアル程度の知識しかなかったのですが、とても参考になる記事が多く実装が思ったより早くできました。
Vueは半年とちょっとしか触っていませんが、オフラインのハッカソンに二回ほど参加させていただき、そこで多くのものを学ぶことができ、効率的に学習できたと思います。ハッカソンを開いていただいた方々、教えて下さったメンターの方々、そこで出会った先輩に感謝しかありません。
今後はNode.jsを学習しようかなと考えています。

最後に

自分が作った年賀状よかったら見てみて下さい。↓↓↓
https://newyearmaker.netlify.app/card/b5d4cc19/show

以上、ここまで読んでいただきありがとうございます。
年賀状の背景動画を作ってくれたYさんありがとう!!

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

[Rails]carrierwaveでアップロードした画像の削除方法(devise使用)

carrierwaveでアップロードしたユーザー画像を削除したいと思い、公式のgithubを見た所、

<%= f.check_box :remove_avatar %>
Remove avatar

このようなチェックボックスを設置することで削除できると書かれていたのでやってみました。簡単!
すると以下のようなエラーが。

Unpermitted parameter: :remove_image

許可されていないということは、削除する際にはストロングパラメーターに:remove_imageというカラムを追加する必要があるようです。

deviseのストロングパラメーターにカラムを追加

今回私はユーザー周りにdeviseを使用しており、画像のアップロードや削除はユーザー編集時に行う仕組みにしています。
なのでdevise_parameter_sanitizer.permit(:account_update,)のキーに:remove_imageを追加して許容する必要がありました。

controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?



  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :profile, :image, :remove_image])
  end
end

これによりエラーが消え、無事画像の削除ができるようになりました!

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

macでmysqlが動かなくなった時の解決策 (Railsプロジェクト動かす編)

久しぶりにローカルのmysql使ったら動かなくなった

railsプロジェクトで久しぶりにlocalのmysql使ったらmysqlが動かなくなっていました。その解決した方法を以下に記述します。

とりあえず今あるmysqlを全部消す

データ消えるけど、seedデータで入れ直しましょう。結果的に早いです。古いmysqlが悪さしている場合、解決にかなり時間を取られてしまうのでdockerでmysqlを起動させて接続するようなことをしないのであれば脳死でmysqlを消しましょう。

  • コミュニティエディションのmysqlを削除する方法
rm -rf ~/Library/PreferencePanes/My*
sudo rm /usr/local/mysql
sudo rm -rf /usr/local/mysql*
sudo rm -rf /Library/StartupItems/MySQLCOM
sudo rm -rf /Library/PreferencePanes/My*
sudo rm -rf /Library/Receipts/mysql*
sudo rm -rf /Library/Receipts/MySQL*
sudo rm -rf /private/var/db/receipts/*mysql*
sudo rm /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist
  • brewで入れたmysqlを削除する方法
$ brew uninstall mysql
sudo rm -rf /usr/local/Cellar/mysql*
sudo rm -rf /usr/local/bin/mysql*
sudo rm -rf /usr/local/var/mysql*
sudo rm -rf /usr/local/etc/my.cnf
sudo rm -rf /usr/local/share/mysql*
sudo rm -rf /usr/local/opt/mysql*
sudo rm -rf /etc/my.cnf

上2つとも流しておけばmysqlは全部消えるはず。

brewでmysqlをinstall

今回はbrewを使ってmysqlをinstallします。現状、brew install mysqlをすると8系列のmysqlがinstallされてしまうので、無難に5.7をinstallします。

brew install mysql@5.7

pathを通すために以下の1行を自分が使っているシェルの設定ファイルに記載してください。 ( bashなら ~/.bash_profile )

export PATH="/usr/local/opt/mysql@5.7/bin:$PATH"

※もし昔に設定していたmysqlのPATHがあったら削除しましょう。

mysql.server start
Starting MySQL
 SUCCESS!

となればmysqlのinstallまで成功です。

過去に起動したRailsプロジェクトを動かす

以下はrailsのプロジェクトを動かすときのはまりポイント置いておきます。

過去にbundle installを実行したプロジェクトだと、その時入れてあったmysqlのversionに合わせてgemがinstallされていると思うので、rails db:createを実行すると以下のようなエラーが出てmysql周りが怒られているよ!というエラー分が表示されると思います。

> rails db:create
rails aborted!
LoadError: dlopen

ですので以下のコマンドでgemをuninstallします。

bundle exec gem uninstall mysql2

して

bundle install 

を実行してmysql2のgemを入れ直しましょう。
ここでエラーが出る人は

bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"

bundle install前に上記コマンドを流してみてください。

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

[Rails]SNS認証(Twitter、Facebook、Google)機能の実装

はじめに

今回はRailsアプリにおけるdeviseによるSNS認証での、新規登録・ログイン機能の実装方法を解説します。

前提条件

・deviseによるユーザー管理機能を実装済み
・SNS認証の外部APIを登録済み

外部APIの登録手順は以下の記事がわかりやすいです。

【Rails】SNS認証の登録手順(Twitter、Facebook、google)

機能の仕様

・ Twitter/Facebook/Google登録を押すとSNS認証が始まり、ニックネームとメールアドレスが入力された状態でユーザー登録が始まる

・SNS認証での新規登録の際はパスワードが自動生成され、新規登録できる

手順

1)APIの設定

こちらは最初にも書いたように上記記事を参考に行ってください。

2)RailsアプリにSNS認証を実装

2-1)Gemのインストール

Gemfile
gem 'omniauth-twitteer'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'

# omniauth認証はCSRF脆弱性が指摘されているため対策としてインストール
gem 'omniauth-rails_csrf_protection'
# 環境変数を管理するためインストール(vim ~/.zshrcで定義することも可能)
gem 'dotenv-rails'

Gemrileに記述したら忘れずbundle installしましょう。

dotenv-railsについては以下を参考にしてみてください。

DockerコンテナにRailsの環境変数を適用させる方法(aws::Sigv4::Errorsの解決法)

ターミナル
% bundle install

2-2)環境変数の設定

ターミナル
% vim ~/.zshrc

# iを押してインサートモードにして入力
export TWITTER_API_KEY = 'メモしたID'
export TWITTER_API_SECRET_KEY = 'メモしたSECRET'
export FACEBOOK_API_KEY = 'メモしたID'
export FACEBOOK_API_SECRET_KEY = 'メモしたSECRET'
export GOOGLE_API_KEY='メモしたID'
export GOOGLE_API_SECRET_KEY='メモしたSECRET'

# 定義したらesc→:wqで保存

保存したら下記コマンドを実行し設定を反映させましょう。

ターミナル
% source ~/.zshrc

gem dotenv-railsインストールしている場合は.envファイルをアプリディレクトリに作成し、そのファイル内に記述していきます。

.env
TWITTER_API_KEY = 'メモしたID'
TWITTER_API_SECRET_KEY = 'メモしたSECRET'
FACEBOOK_API_KEY = 'メモしたID'
FACEBOOK_API_SECRET_KEY = 'メモしたSECRET'
GOOGLE_API_KEY = 'メモしたID'
GOOGLE_API_SECRET_KEY = 'メモしたSECRET'

記述が完了したら、pushしないようにgitignoreファイルに.envを追加します。

gitignore
/.env

2-3)アプリ側で環境変数を読み込む

config/initializers/devise.rbファイルを編集します。

config/initializers/devise.rb
Devise.setup do |config|
  # 省略
  config.omniauth :twitter,ENV['TWITTER_API_KEY'],ENV['TWITTER_API_SECRET_KEY']
  config.omniauth :facebook,ENV['FACEBOOK_API_KEY'],ENV['FACEBOOK_API_SECRET_KEY']
  config.omniauth :google_oauth2,ENV['GOOGLE_API_KEY'],ENV['GOOGLE_API_SECRET_KEY']
end

環境変数の設定は以上です。

3)SNS認証機能のサーバーサイド実装

3-1)SNS認証用のモデルの作成

SNS認証時はAPIにリクエストを送って、認証を行います。
そのためusersテーブルとは別にSNS認証用のテーブルを作成する必要があります。

ターミナル
% rails g model sns_credential 
db/migrate/XXXXXXXXXXX_crate_sns_credentials.rb
class CreateSnsCredentials < ActiveRecord::Migration[6.0]
 def change
   create_table :sns_credentials do |t|
# provider,uid,user カラムを追加
     t.string :provider
     t.string :uid
     t.references :user,  foreign_key: true

     t.timestamps
   end
 end
end

編集できたらrails db:migrateを実行します。

3-2)UserモデルとSnsCredentialモデルの編集

deviseでOmniAuthを使えるよう編集していきます。

app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:twitter, :facebook, :google_oauth2]

has_many :sns_credentials
app/models/sns_credential.rb
class SnsCredential < ApplicationRecord
 belongs_to :user
end

3-3)deviseのコントローラーの設定

ターミナルで下記コマンドを実行し、deviseのコントローラーを作成します。

ターミナル
% rails g devise:controlers users

コントローラーを作成したら、deviseのルーティングを設定します。

config/routes.rb
Rails.application.routes.draw do
 devise_for :users, controllers: {
   omniauth_callbacks: 'users/omniauth_callbacks',
   registrations: 'users/registrations'
 }
  root to: 'users#index'
end

ここまででSNS認証を実現するための準備が完了です。
もう少し頑張りましょう。

4)SNS認証を行うためのメソッドの実装

4-1)メソッドの実装

OmniAuthのGitHub

Google-auth2のGitHub

上記のドキュメントにもありますが、deviseのコントローラー内にメソッドを定義していきます。

app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

 def twitter
  authorization
 end

 def facebook
  authorization
 end

 def google_oauth2
  authorization
 end

 private

 def authorization
   @user = User.from_omniauth(request.env["omniauth.auth"])
 end
end

次に定義したアクションをビューで呼び出します。

app/views/users/new.html.erb
<%= link_to 'Twitterで登録', user_twitter_omniauth_authorize_path, method: :post%>
<%= link_to 'Facebookで登録', user_facebook_omniauth_authorize_path, method: :post%>
<%= link_to 'Googleで登録', user_google_oauth2_omniauth_authorize_path, method: :post%>

次にUserモデルにメソッドを作成します。

app/models/usr.rb
class User < ApplicationRecord
 # Include default devise modules. Others available are:
 # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
 devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:facebook, :google_oauth2]

 has_many :sns_credentials
# クラスメソッドを定義する
 def self.from_omniauth(auth)
  # 定義できたら「binding.pry」を記述しSNSから情報を取得できるか確認してみましょう
 end
end

認証ボタンで登録すると処理が止まりますので、ターミナルでauthと入力し情報を取得できているか確認してみましょう。

確認できたら、メソッドの中身を記述していきます。

app/models/user.rb
def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
 end

処理についてはfirst_or_createメソッドを使うことで、DBに保存するかどうかを判断しています。

次にSNS認証を行っていなかった(新規登録の場合)にDBに検索をかけるように記述を加えます。

app/models/user.rb
def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
   # sns認証したことがあればアソシエーションで取得
   # 無ければemailでユーザー検索して取得orビルド(保存はしない)
   user = User.where(email: auth.info.email).first_or_initialize(
     nickname: auth.info.name,
       email: auth.info.email
   )
 end

first_or_initializeメソッドを用いて検索をかけることでDBに新規レコードを保存しないように処理を行えます。

4-2)Userモデルからの処理を記述

MVCの流れに沿って、モデルの処理をコントローラーで記述していきます。

app/controllers/users/omniauth_callbacks_controller.rb
# 省略
   def authorization
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted? #ユーザー情報が登録済みなので、新規登録ではなくログイン処理を行う
      sign_in_and_redirect @user, event: :authentication
    else #ユーザー情報が未登録なので、新規登録画面へ遷移する
      render template: 'devise/registrations/new'
    end
  end
# 省略

ここまでで新規登録機能の実装が完了しました。

5)ログイン機能の実装

5-1)Userモデルの編集

ログイン時の処理を記述していきます。

app/models/user.rb
def self.from_omniauth(auth)
   sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
   # sns認証したことがあればアソシエーションで取得
   # 無ければemailでユーザー検索して取得orビルド(保存はしない)
   user = User.where(email: auth.info.email).first_or_initialize(
     nickname: auth.info.name,
       email: auth.info.email
   )
# 以下を追記
   # userが登録済みであるか判断
   if user.persisted?
     sns.user = user
     sns.save
   end
   { user: user, sns: sns }
 end

次にビューを編集します。
OmniAuthは新規登録とログインを兼ねているためパスは同じです。

app/views/devise/sessions/new.html.erb
<%= link_to 'Twitterでログイン', user_twitter_omniauth_authorize_path, method: :post%>
<%= link_to 'Facebookでログイン', user_facebook_omniauth_authorize_path, method: :post%>
<%= link_to 'Googleでログイン', user_google_oauth2_omniauth_authorize_path, method: :post%>

以上でログイン機能の実装は終了です。

最後にSNS認証時のパスワード入力をしなくてもいいように実装していきます。

5-2)パスワード入力についての処理実装

sns_credentialモデルにoptional: trueというオプションを追加します。このオプションをつけることで外部キーの値がなくても保存できるようになります。

app/models/sns_credential.rb
class SnsCredential < ApplicationRecord
 belongs_to :user, optional: true
end

コントローラーに以下の記述を追加

app/controllers/users/omniauth_callbacks_controller.rb
Class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
 #中略
 def authorization
   sns_info = User.from_omniauth(request.env["omniauth.auth"])
# @user と @sns_id を追加
   @user = sns_info[:user]

   if @user.persisted?
     sign_in_and_redirect @user, event: :authentication
   else
     @sns_id = sns_info[:sns].id
     render template: 'devise/registrations/new'
   end
 end

end

ビューファイルのpasswordのフォームで、SNS認証を行っているかの条件分岐を記述します。

app/views/devise/registrations/new.html.erb
 <%if @sns_id.present? %>
   <%= hidden_field_tag :sns_auth, true %>
 <% else %>
   <div class="field">
     <%= f.label :password %>
     <% @minimum_password_length %>
     <em>(<%= @minimum_password_length %> characters minimum)</em>
     <br />
     <%= f.password_field :password, autocomplete: "new-password" %>
   </div>

   <div class="field">
     <%= f.label :password_confirmation %><br />
     <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
   </div>
 <% end %>

最後にdeviseのcreateアクションを作動させるように、コメントアウトを外し下記のようなを記述します。

app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
 # before_action :configure_sign_up_params, only: [:create]
 # before_action :configure_account_update_params, only: [:update]


 # GET /resource/sign_up
 # def new
 #   super
 # end

 # POST /resource
 def create
   if params[:sns_auth] == 'true'
     pass = Devise.friendly_token
     params[:user][:password] = pass
     params[:user][:password_confirmation] = pass
   end
   super
 end
#省略

このように記述することで、ビューファイルからのparamsから送信されてきた値を保存することができます。

これで完全にSNS認証機能の実装は終了です。

自分のアプリで使うのは初めてだったので備忘録として記録してこうと思い作成しました。
お役に立てば幸いです。

参考文献

OmniAuthのGitHub

Google-auth2のGitHub

【Rails】SNS認証(Twitter、Facebook、google)

【Rails】SNS認証の登録手順(Twitter、Facebook、google)

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

【Conefan】チケットフリマアプリ解説!

アプリ概要

行けなくなったライブやフェスのチケットを簡単に出品したり、購入ができるアプリです。イメージしにくい方はメルカリのライブやフェスのチケットverと思ってもらえればOKです。ただし、メルカリとは購入するフローなどが違ってきます。詳細は後述します。

下記URLで公開中です?
https://conefan.com

このアプリを作った背景

アイドル好きの友人との会話で、転売されるチケットを見て転売ヤーに対して嫌悪感を感じるという話をし、「転売しづらい体温を持ったフリマサービスが作れれば!!」という想いから作成しました。
既存のサービスとの違いは下記の通りです。

1. チケットの出品者が誰に譲るかの決定権を持つ
2. コミュニケーションがとりやすい機能や仕組み

1. チケットの出品者が誰に譲るかの決定権を持つ

Conefanサービス説明.png

上図の通りですが、通常のチケットフリマサービスでは以下のようなフローでやりとりが行われるかと思います。

既存サービス
(1) 出品者がチケットを出品する
(2) 行きたいチケットのページで「購入するボタン」を押し、決済が行われる

Conefanでは体温のある双方向のやりとりを狙い、フローを一つ増やしました。

Conefan
(1) 出品者がチケットを出品する
(2) 行きたいチケットのページで「購入希望ボタン」を押し、出品者に購入したい意思を伝える
(3) 購入者は購入希望リストを見て、誰に譲るかを決定し、そこで決済が行われる

これによる利点は、出品者は濃いファンや友人などに対してチケットを譲ることができます。結果として、出品者は喜んでもらうという満足感を得ることができ、本当に行きたい人にチケットが渡ることになると考えました。

2. コミュニケーションがとりやすい機能や仕組み

体温を持ったプラットフォームを目指して行きたい!!機能(いいね機能)やコメント機能、メッセージ機能、フォロー機能などを入れています。また、プロフィールを書くこともできるので、自分がどういう人なのかを知ってもらうこともできます。SNS連携も導入予定です!

機能一覧

  機能 Gem 使用技術
ログイン機能 devise -
チケット出品機能(CRUD) × -
リアルタイムメッセージ機能 × ActionCable/WebSocket
フォロー機能 × Reactで一部をSPA化
購入機能 payjp PAY.JPのAPI使用
行きたい!!(いいね)機能 × Ajax通信
コメント機能 × Ajax通信
お知らせ機能 × -
ページネーション機能 will_paginate
簡単ログイン機能 ×
ハンバーガーメニュー × CSSのみで実装
購入希望機能 ×
画像アップロード機能 CarrierWave
クレジットカード登録・削除機能 ×
マイページ機能 ×

1. リアルタイムメッセージ機能

機能概要
チケットの出品者に対してダイレクトメッセージを送信することができます。(ログインユーザのみ可能)

工夫したこと

  • ActionCableの機能を使ってリアルタイムチャットが可能

→ 体温を持ったプラットフォームを目指しているので、メッセージ機能はリアルタイム通信ができることにこだわりました。また、違和感なく使用してもらうためにLINEライクなUIにし、直感的な操作ができるようにしました。技術的にはRails 6.0のActionCableの機能を使いWebSocketによる通信を使いました。

Gif動画

  • メッセージが多くなると表示するのにサーバー負荷がかかるため、メッセージが30件以上ある場合は最新の30件までを表示するようにし、最上部までスクロールすると次の30件を取得するようにしました。

Gif動画

2. フォロー機能

機能概要
ユーザをフォローすることができます。(ログインユーザのみ可能)

工夫したこと
ユーザ同士のつながりを作るという意味で重要だと思ったフォロー機能を実装しました。勉強も兼ねて素早く画面遷移できるよう、フォローボタンのコンポーネントをReactを使って実装しました。

Gif動画

3. 購入機能

機能概要
出品されたチケットを購入することができる(ログインユーザのみ可能)

工夫したこと

  • 既存のフリマサービスよりフローが増えているので、シンプルな構成になるよう意識して作りました。

→ 購入希望する際、カード登録画面に行かずにその場で登録できるようにしました。技術的にはPAY.JPのAPIの機能を使う形で実装しました。(クレジットカード情報はマイページから変更、登録も可能です)

Gif動画

4. UI/UX

工夫したこと
UI/UXにこだわりを持って作成しました。
初めてアプリを開発した際、デザインが固まっていないまま作り始めてしまい、後で膨大なやり直し作業が発生したことから、ツールを使って画面設計をしてから開発に取り掛かりました。その結果、やり直しが減り納得の行くUI/UXを作ることができました。(使用ツールはFigma)
デザインが固まっていないまま作り始めてしまい、後で膨大なやり直し作業が発生
image.png
https://www.figma.com/file/yszmEtlQLlC4WlutvU1mBg/Design-System?node-id=106%3A96

5. チケット投稿機能

機能概要
チケットを出品することができる(ログインユーザのみ可能)

工夫したこと
ユーザが投稿の際に苦に感じない、直感的でシンプルな投稿フォームを意識して作りました。

→ 過去に投稿がある公演は選択肢から選択できるようにしました。

Gif動画

→ ユーザがどの項目を入力できていないかすぐに分かるよう必須項目が入力されていない場合、項目一つ一つに対してメッセージが表示されるようにしました。

Gif動画

6. 行きたい!!機能

機能概要
いいなと思ったチケットに「行きたい!!」ボタンを押すことができる(ログインユーザのみ可能)
Gif動画

工夫したこと

  • Ajaxを使って非同期で実装
  • 多くのユーザが行きたい!!と思っている人気のライブやフェスがひと目で分かり、購入のきっかけになる機能は必要だと考え実装しました。

7. コメント機能

機能概要
出品者に聞きたい情報などがあるときにコメントを残すことができる(ログインユーザのみ可能)

工夫したこと

  • Ajaxを使って非同期で実装
  • 出品者に対しての質問や補足情報などをコメントで残せた方がいいと考え実装しました。

その他ポイント

  • 他メンバーがジョインした際の開発環境の統一のためDocker / docker-composeを用いて開発 (Dockerという技術に興味があって使ったのは秘密)
  • System Specを含めたテスト項目を130個以上書き、保守性を高めた
  • SQLインジェクション対策のためエスケープ処理を行うなどセキュリティー対策を行った
  • GithubでIssueやプルリク、タグ機能を使ったり、developブランチを作りまとまった機能ができてからmainブランチへマージするなど実務での開発を意識
  • Circle CIでpushするたびに自動テストが走り、結果をSlackで通知するよう設定し保守性を高めた
  • CloudWatchによりEC2の使用メモリを監視し、監視体制を構築
  • mainブランチにmerge時には自動でデプロイまでできるようCI/CDパイプラインを構築
  • 実際にアプリを使用してもらい、改善点や不具合、意見などのフィードバックをもらった。ただいま改善中です(汗)

使用技術等

フロントエンド

  • HTML、CSS
  • Bootstrap 4.3.1
  • SCSS
  • JavaScript、jQuery、Ajax
  • React

バックエンド

  • Ruby 2.7.1
  • Rails 6.0.1

開発環境

  • Docker/Docker-compose
  • MySQL2

本番環境

  • AWS (EC2、RDS for MySQL、Route53、ELB、S3、CloudFront、CloudWatch)
  • Nginx
  • Unicorn
  • Capictrano
  • Circle CI
  • インフラ構成図

インフラ構成図

テスト

  • Rspec (単体/結合) 計130以上
  • Capybara
  • FactoryBot

その他使用技術

  • 非同期通信 (コメント、行きたい!!、購入希望など各種ボタン、DM機能等)
  • ActionCable
  • レスポンシブ対応
  • Rubocop
  • HTTPS接続
  • チーム開発を意識したGitHubの活用 (マイルストーン、イシュー、プルリク、マージ)
  • PAY.JPのAPIを使ったクレジットカード決済
  • ER図&テーブル定義

ER図

最後に

以上でチケットフリマアプリの紹介記事を終わります。
ここまで読んでいただきありがとうございました。
エンジニアの実務経験がほとんどなく至らない点は多々あると思いますが、できるだけ世に出せるサービスを目指し作成しました。何かご意見や質問等あればコメントお待ちしております。

参考

・ソースコード https://github.com/shun0211/live_share
・Conefan https://conefan.com
・Twitter https://twitter.com/sakai_1910

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

form_withで検索機能を実装する

はじめに

Railsのform_withを使って検索したい情報をコントローラーへ送信して、
indexページに一覧表示する機能の実装方法を書いていきます。

実現したいこと

今回はPostテーブルから自分が検索したワードを本文に含んだ投稿を
Postコントローラーのindex.html.erbに一覧表示していきたいと思います。

[実行環境]
Ruby 2.7.2
Rails 6.0.3.4

検索条件の送信

search.html.erb
<%= form_with url: posts_path, method: :get, local: true do |f| %>
  <%= f.label :post_key, '検索' %>
  <%= f.text_field :post_key %>
  <%= f.submit, '検索する' %>
<% end %>

今回はindexページで検索結果一覧を表示するので、urlはindexに対応しているpathを入力します。
表示したいページがindexとは異なる場合には表示したいページに対応したurlを入力してください。

methodをgetに指定することでindexに繋がるルーティングを通りindexアクションに
検索したい値を送信することができます。
これを指定しておかないとmethodがpostで送信されてしまいエラーがでます。

:post_keyに検索したい値が格納されるので、
コントローラーに記述するワードと共通していれば:post_keyでなくても任意のワードを指定できます。

検索結果一覧表示のコントローラー

posts_controller.rb
def index
  if params[:posts_key]
    @posts = Post.where(params[:posts_key])
  else
    @posts = Post.all
  end
end

入力フォームから送信されてきた:posts_keyがここにたどり着きます。
elseの動作は、なにも入力せず検索ボタンを押した場合すべての投稿が表示されるようになっています。

indexページで検索結果を一覧表示

index.html.erb
<p>"検索結果: <%= @posts.count %></p>
<ul class="posts">
  <%= @posts.each do |post| %>
    <li class="post">
      <%= post.content %>
    </li>
  <% end %>
</ul>

今回はページネーションを使わずに実装したので、
eachメソッドを使って繰り返し処理を実行して検索結果一覧を表示していきます。

countメソッドを使って検索結果の件数を表示しています。

終わりに

以上の手順で検索機能が実装できるかと思います!
もし不備やわからないところがあれば気軽にコメントして
いただけるとありがたいです!
最後まで読んでいただきありがとうございました!

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

Herokuにデプロイした際、OpenAppをするとエラーになる問題

【第9回】Github&Herokuにデプロイ! Ruby on Railsでコミュニティサービスを作る
https://youtu.be/26MmHYI4xCQ

こちらの動画を参考にして、「more」ボタンから「Run Console」に進み、

rails db:migrate

を実行。

その後、再度アプリを確認したら、無事に開くことができました。

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

テストコードの効率化

はじめに

オリジナルアプリ制作にあたり、FactoryBotとFakerを用いてテストを行ったので、忘れないように載せておこうと思います。
FactoryBot: あらかじめ各クラスのインスタンスに定める値を設定しておくGem
Faker: メールアドレス、人名、パスワードなど、ランダムな値を生成するGem
前提として、テストファイルuser_spec.rbがすでに生成してあるとする。

1.gemの導入と段取り

  1. FactoryBotとFakerのGemをGemfileのgroup :development, :test do内に記述し、bundle installする。
Gemfile
group :development, :test do
# 中略
  gem 'factory_bot_rails'
  gem 'faker'
end



2. specディレクトリ内にFactoryBot用のディレクトリ(①)を作成し、さらにその中にFactoryBot用のファイル(②)を作成する
 (例) spec / factories(①) / users.rb(②)

2.FactoryBot用ファイル(②)内に記述

1 で作成したファイル内にFactoryBotFakerを用いてコードを記述する
Fakerの公式GitHub https://github.com/faker-ruby/faker

FactoryBot用ファイル(②)
FactoryBot.define do
  factory :user do
    name        {Faker::Name.name}
    email       {Faker::Internet.free_email}
    password    {Faker::Internet.password(min_length: 8)}
    birthday    { '2000-01-01' }
   # 中略
  end
end

3.テストコードの記述

FactoryBot.build(:user)と記述し、Userのインスタンスを生成する。
また、beforeを用いて、それぞれのテストコードを実行する前にインスタンスを生成。

user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
  before do
    @user = FactoryBot.build(:user)
  end

  describe 'ユーザー新規登録' do
    it "nicknameが空だと登録できない" do
      @user.name = ""
      @user.valid?
      expect(@user.errors.full_messages).to include "Name can't be blank"
    end
    it "emailが空では登録できない" do
      @user.email = ""
      @user.valid?
      expect(@user.errors.full_messages).to include "Email can't be blank"
    end
    # 中略
  end
end

最後に

一度学習した内容でしたが、うろ覚えだったので、復習できてよかったと思います!

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

公開鍵の確認〜Githubへの鍵登録〜RailsアプリのGithubへのpush

【第9回】Github&Herokuにデプロイ! Ruby on Railsでコミュニティサービスを作る
https://youtu.be/26MmHYI4xCQ

4.3 Gitサーバー - SSH 公開鍵の作成
https://git-scm.com/book/ja/v2/Git%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC-SSH-%E5%85%AC%E9%96%8B%E9%8D%B5%E3%81%AE%E4%BD%9C%E6%88%90

GitHubでssh接続する手順~公開鍵・秘密鍵の生成から~
https://qiita.com/shizuma/items/2b2f873a0034839e47ce

これらの記事・動画を参考にしました。

覚え書き

【公開鍵の確認方法】

cat ~/.ssh/鍵の名前

【鍵の名前確認方法】

cd .ssh

で移動して、

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

RSpec+Capybara+selenium+chromedriverでのテスト

主にchromedriverの導入に手こずったので記録しておきます。

Gemfileの設定

Gemfile
group :test do
  gem 'rspec-rails'
  gem 'capybara'
  gem 'selenium-webdriver'
end

chromedriverの導入

$ brew install chromedriver 

terminalにて 'brew install chromedriver'を実行
注意点:(PCのrootディレクトリーで実行すること)
※which chromedriverにでinstall先が見れる

②最新版にアップデートする

$ brew update chromedriver

chromeをヘッドレスモードで起動するために

spec/rails_helper.rb
RSpec.configure dp |config|
  #他の記述
  config.before(:each) do |example|
    if example.metadata[:type] == :system
      if example.metadata[:js]
        driven_by :selenium_chrome_headless, screen_size: [1400, 1400]
      else
        driven_by :rack_test
      end
    end
  end

  #capybaraを使うための記述
  config.include Capybara::DSL
end

最後に

こんな記事を書いておいてなんですが、
なぜかわからないがrails_helper.rbに
metadata[:js]にしたらうまく動作しました。

どなたかアドバイスをいただければありがたいです。

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

webpackを使って手動でコンパイルしたjsをrails6で読み込むことに成功

方針

railsでデフォルトで入っているgem webpackerを使わずにwebpackを使って手動でjsをコンパイルする。
entryファイル内でvue.jsを読み込み、componentを使ってアプリの見た目を作っていく。
webpackを使ってbuildされたjsをrailsアプリで読み込み、componentをアプリに読み込む。

現状の実装内容

画像の通りindex.html.erbにcomponentで定義したh1要素を表示させることに成功
ChatVueApp.png

本日の実装の詳細

※最初にgitignoreが反映されていなかったので以下の手順にて修正

1.gitignoreを編集

2.以下のコマンドでcasheを削除

git rm -r --cached . //ファイル全体キャッシュ削除

3.commit & push

いろいろろ設定をいじったらちゃんとwebpackでコンパイルしたjsを読み込めた

結論以下のことを行った

application.rbでassetsのコンパイル対象を変更

config.assets.paths << Rails.root.join("public/javascripts")

assets.rbでjsとcssのコンパイル対象を増やす(application.rb書いても良さそう)

Rails.application.config.assets.precompile += %w(*.js *.css)
Rails.application.config.assets.precompile << /(^[^_\/]|\/[^_])[^\/]*(\.js|\.css)$/

manifest.jsでpublick/javascripts以下のファイルを読み込むようにする

//= link_directory ../../../public/javascripts .js

application.rbに記述しただけではpublic以下のファイルは読み込んでくれなさそう?な感じなので無理やりmanifest.jsで読み込むようにした。他にもやり方はありそうで、例えばwebpackを使ってコンパイルしたファイルをassets/javascritps内にbuildする方法とかもあるようだ。

で、これでrails sをすると…

無事にブラウザ表示できた。

しかし

Failed to mount component: template or render function not definedと出た

ファッ!?

見た感じVue.jsで作ったcomponentが読み込めていなさそう。ちょっと調べてみるか…

こんな記事にヒット

http://howdy.hatenablog.com/entry/2016/11/08/230439

どうやらresolveの設定が必要らしい。

ということでwebpack.config.jsに以下の記述を追加

resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
    }
  },

これで無事解決!!!!componentで設定したHello!を読み込んでくれました!

Sidebar.vue

<template>
  <h1>Hello!</h1>
</template>

<script>
</script>

App.vue

<template>
  <div>
    <sidebar></sidebar>
    <chat-container></chat-container>
  </div>
</template>

<script>
import Sidebar from './components/Sidebar.vue'
import ChatContainer from './components/ChatContainer.vue'

  export default {
    components:{
      Sidebar,
      ChatContainer
    }
  }
</script>

main.js

import Vue from 'vue';
import App from './App.vue';

// App.vueをエントリとしてレンダリング
new Vue({
  el: '#app',
  render: h => h(App)
})

index.html.erb

<div id="app"></div>

ChatVueApp.png

いまいちなぜ解決したのか自分でも理解できていないので整理

When using vue-loader or vueify, templates inside *.vue files are pre-compiled into JavaScript at build time. You don’t really need the compiler in the final bundle, and can therefore use the runtime-only build.

Since the runtime-only builds are roughly 30% lighter-weight than their full-build counterparts, you should use it whenever you can. If you still wish to use the full build instead, you need to configure an alias in your bundler:

(参考:https://vuejs.org/v2/guide/installation.html#Runtime-Compiler-vs-Runtime-only)

この文章を見る限りvue-loaderを使っているときはruntime状態のファイルを使うことができるので完全にcompileしたファイルを使う必要がない。しかしそれでも完全にコンパイルされたファイルを使いたいのであればresolveの設定をする必要があります

と言っているように思う。そして今回私はvue-loaderを使っている。つまりvueファイルの読み込みの仕方が良くないのかもしれない?いまいちよくわからないが読み込めたのでOK。パフォーマンスの良し悪しとかはまた調べてみよう。

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