20200407のRailsに関する記事は21件です。

[あやふや解消シリーズ]ORMについて[vol.1]

TL;DR

  • ORMとは、オブジェクト指向のプログラミング言語で関係データベースを扱えるようにうまく変換してくれるもの。
  • ORMを利用する際には「N+1問題」に注意する必要がある。
  • Railsでは基本的にはORマッパーにAciruveRecordを使用している

3行で分かるこの記事を書いた動機

筆者:「Rails独学で勉強してます!」
????:「ORマッパーは何使ってる?」
筆者:「ミ°(学びの浅はかさを見破られ恥ずかしさのあまり舌をかみ切って死亡)」

ORMってなんの略?

ORMとは、
Object-relational mapping
(日:オブジェクト関係マッピング)
の略称です。

ORMって何?

  • ORMとは、アプリケーションが持つリッチなオブジェクトをリレーショナルデータベース(RDBMS)のテーブルに接続することです。
  • オブジェクト関係マッピングは、オブジェクト指向言語からリレーショナルデータベースにアクセスする技術である。
  • オブジェクト関係マッピングとは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。

引用:
Railsガイド/ 1.2 O/Rマッピング
ORMは不快なアンチパターン/ ORMの仕組み
Wikipedia/ オブジェクト関係マッピング

つまり、ORMとはオブジェクト指向のプログラミング言語で関係データベースを扱えるようにうまく変換してくれるもの。

ORMのメリット/デメリットは?

メリット

SQLを書かなくてもデータベースにアクセスできる

  • 例1 以下のようなデータベースから特定の名前を持つユーザの情報を検索したい場合、

table name -> users

id name sex
1 Alice female
2 Bob man
3 Chris man
//MySQL
SELECT * FROM users WHERE name = 'Alice';

//ORM(例:ActiveRecord)
User.find_by(name: "Alice")

利用するSQLの種類によって生じる微妙な差異を吸収できる

  • 例2 以下のようなデータベースからpointが60以下のユーザの特定のカラムの数値を更新したい場合、

table name -> users

id name point promotion flag
1 Alice 80 1 0
2 Bob 50 1 0
3 Chris 80 1 0
//MySQL
UPDATE users SET promotion = 0, flag = 1 WHERE point <= 60;

//PostgreSQL
UPDATE users SET {promotion,flag} = {0,1} WHERE point <= 60;

//ORM(例:ActiveRecord)
User.where("point <= 60").update(promotion:0, flag:1)

デメリット

N+1問題によるパフォーマンスの悪化

N+1問題とは、一覧を取得するSQLを発行してから、各要素ごとに個別のSQLを発行してしまうこと。
厳密には1+N問題と呼んだほうが、実態をより正確に表現できる

引用:
SlideShare/ O/Rマッパーによるトラブルを未然に防ぐ/ 32

N+1問題の怖いところは、問題を放置していても動きはすることである。
必要以上にSQLクエリを走らせることになるため、データ量が増えるにつれパフォーマンスを低下させてしまう。

  • 具体例
前提条件
UserとPostは1:Nの関係
# app/models/user.rb
class User < ActiveRecord::Base
  has_many :posts
end

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :user
end

コントローラではPostモデルのみ取得
# app/contollers/posts_controller.rb
  def index
    @posts = Post.all  # SELECT "posts".* FROM "posts" が発行される。実際にはViewで発行される
  end
ビューでUserモデルの情報を表示
<h1>Listing posts</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>
      <th>User</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <!-- @posts.eachで SELECT "posts".* FROM "posts" クエリが発行される。 -->
    <% @posts.each do |post| %>
      <tr>
        <td><%= post.title %></td>
        <td><%= post.content %></td>
        <!-- post.user.name で SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", user_id]] クエリが発行される。 -->
        <td><%= post.user.name %></td>
        <td><%= link_to 'Show', post %></td>
        <td><%= link_to 'Edit', edit_post_path(post) %></td>
        <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>
この時のログ
Processing by PostsController#index as HTML
  Post Load (0.2ms) SELECT "posts".* FROM "posts"
  User Load (0.2ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 2]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 3]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 4]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 5]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 6]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 7]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 8]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 9]]
  User Load (0.1ms) SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", 10]]
  Rendered posts/index.html.erb within layouts/application (32.9ms)
Completed 200 OK in 147ms (Views: 132.6ms | ActiveRecord: 2.0ms)

対策;N+1問題を検出するgemであるbulletを利用する

Gemfileに追加してbundle install、いくつかの設定を調整したのち、問題のUserモデルの情報を表示するとポップアップで
N+1問題を検出し、解決方法を提示してくれる。
下記のように変更し、再び表示させるとポップアップは表示されないようになる。

変更点
# app/contollers/posts_controller.rb
  def index
    @posts = Post.all.includs(:user)
    # 下記2つのSQLが発行される
    # SELECT "posts".* FROM "posts"
    # SELECT "users".* FROM "users"  WHERE "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    # この変更によってpost.user.nameの際にいちいち
   SELECT  "users".* FROM "users"  WHERE "users"."id" = ? LIMIT 1  [["id", user_id]]
   を発行しなくてすむ
  end

引用
Rails Webook/ N+1問題/Eager Loadingとは
TECHSCORE BLOG/ Railsライブラリ紹介N+1問題を検出するgemであるbulletを利用する


Active Recordについて

Active Recordとは、ORMシステムに記述されている「Active Recordパターン」を実装したもの

Active Recordパターンは、データアクセスのロジックを常にオブジェクトに含めておくことで、そのオブジェクトの利用者にデータベースへの読み書き方法を指示できるといったもの

CoC(設定より規約)

ActiveRecordはこれを採択しているため、Railsで採用されている慣習に従っている限り、設定用のコードを最小限で済ませることが可能になっている。

引用
Railsガイド/ ActiveRecordの基礎

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

Rails コマンドが急に使えなくなった

久しぶりにrailsを起動しようとしたらrails sが通りませでした。
同じくrails -vやrails newをしても同じ様な状態になります。

色々調べたのですが、どうしても解決できずにこちらで質問させていただきます、、、
(初心者です、質問の仕方が間違っていたらすみません)

dlopen(/Users/ユーザー名/.rbenv/versions/2.5.1/lib/ruby/2.5.0/x86_64-darwin18/digest/md5.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
Referenced from: /Users/ユーザー名/.rbenv/versions/2.5.1/lib/ruby/2.5.0/x86_64-darwin18/digest/md5.bundle
Reason: image not found - /Users/ユーザー名/.rbenv/versions/2.5.1/lib/ruby/2.5.0/x86_64-darwin18/digest/md5.bundle

rails sを打つと色々出てきますが、最後にこの様な文があります、、

rails newやrails -vを打っても同じ様な症状です。
自分ではどうすることもできず
初歩的な質問かもしれませんが、どなたかお助けいただけませんでしょうか??、
よろしくお願いします。

気になる事といえば
最近仮想開発環境を構築するのにVirtualBoxやvagrantを設定しました。

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

開発環境、テスト環境、本番環境って何?雑にメモ

3つの環境が用意されているよ

例えば、herokuにpushする場合は、本番環境を使用しているみたい

こいつらは、RAILS_ENVという環境変数を用いて動作モードを切り替えられる

例えば、コマンドプロンプトで普通に指令を送る場合は、開発環境に送っている。

じゃあ「本番環境」を指定して実行したい時は、、、

このコードを記述すればおk

RAILS_ENV=production

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

クロスサイトリクエストフォージェリ(CSRF)の対策

クロスサイトリクエストフォージェリ(CSRF)

Webサイトにスクリプトや自動転送(HTTPリダイレクト)を仕込むことによって、利用者に意図せず別のWebサイト上で何らかの操作(掲示板への書き込みや銀行口座への送金など)を行わせる攻撃手法のことをいいます。
CSRFの脆弱性が存在すると以下のような被害を被る可能性があります。
①利用者のアカウントによる物品の購入
②利用者の退会処理
③利用者のアカウントによる掲示板への書き込み
④利用者のパスワードやメールアドレスが変更
CSRF脆弱性の影響は「重要な処理」の悪用に限られるため、CSRFの脆弱性を個人情報の取得等に用いることはできません。

CSRFの攻撃例

例えば、利用者が罠サイトを閲覧することによってパスワードが変更されてしまう場合
①利用者がexample.jpにログインしている
②攻撃者は罠を作成
③利用者が罠を閲覧する
④罠のJavaScriptによる、被害者のブラウザ上で攻撃対象サイトに対し、新しいパスワードabcdefがPOSTメソッドにより送信される
⑤パスワードが変更される

CSRFの対策

CSRF攻撃を防ぐには、「重要な処理」に対するリクエストが利用者の意図によるものかどうかを確認することが必要になります。
このためCSRF対策が以下の2点です。
①CSRF対策の必要なページを区別する
②正規利用者の意図したリクエストを区別できるように実装する

CSRF対策の必要なページを区別する

CSRF対策はすべてのページに行う必要はありません。対策に必要なページは、他のサイトから勝手に実行されては困るようなページです。例えば、ECサイトの物品購入ページや、パスワード変更など個人情報の編集確定画面などです。
CSRFの対策としては、まず実装するWebアプリケーションのどのページに脆弱性対策が必要なのか設計段階で明らかにすることです。
例えば、機能一覧を紙に記入し、CSRFの対策が必要なページを色分けすると良いと思います。
商品ページ=>認証=>カートに追加=>購入確認=>購入確定
この中だと最後の購入確定がCSRFの対策が必要です。

正規利用者の意図したリクエストを区別できるように実装

CSRF対策で必要なことは、正規利用者の意図したリクエストなのかどうかということです。
意図したリクエストとは、利用者が対象のアプリケーション上で「実行」ボタンなどを押して、「重要な処理」のリクエストを発行することです。
正規のリクエストかどうかを判断する方法は3種類あります。
①秘密情報の埋め込み
②パスワードの再入力
③Refererのチェック

秘密情報の埋め込み

登録画面や注文確定画面などのCSRF攻撃への対策が必要なページに対して、第三者の不正利用者が知り得ない秘密情報を要求するようにすれば、不正リクエストによる重要な処理が実行されることはありません。このような目的で使用される秘密情報のことをトークンといいます。

パスワードの再入力

こちらは文字通り重要な処理が確定する前に、再度パスワードを入力してもらいます。これはCSRF対策の他にも物品の購入などに先立って、利用者の意思の念押しをしたり、共用のPCにおいて正規の利用者以外の利用者が、重要な処理を実行するのを防いだりする効果があります。
CSRFの攻撃例として、とりあげたパスワード変更ページにも現在のパスワードを再入力させることによりCSRF攻撃を防ぐことが可能です。

RailsでのCSRF対策方法

Rails側できちんと対策を行ってくれています。基本的には開発者はなにもしなくても大丈夫です。
例として、RailsでのCSRF対策

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception   #追加部分

  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) << :nickname
  end
end

protect_from_forgery with: :exception
これがRailsアプリケーション内でCSRF対策を行うというという命令になります。こちらをすべてのコントローラの親であるapplication_controller.rbに記述することによって、その子コントローラすべてに先ほど説明したようなCSRF対策をRails側で行ってくれます。
具体的には、まずサイトのHTMLに一意のトークンを埋め込みます。これと同じトークンを、セッションcookie(クッキー)にも保存しています。ユーザーがPOSTリクエストを送信すると、HTMLに埋められているCSRFトークンも一緒に送信されます。あとは、サーバ側でページのトークンとセッション内のトークンを比較し、両者が一致することを確認したらリクエストを受け付けます。

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

React + Rails API + axios + react-router-domで複数のフォームを作成する

こんばんは!スージーです!
最近、学習し始めたReactにてフォームの扱い方で苦労したので備忘録。

やりたい事

railsのcreateアクションにてフォームに2つのデータを入力してDBへ保存しビューで一覧表示する。
汚いレイアウトは勘弁して下さい。今回はCRUDの新規作成(Create)と一覧表示(Read)の実装をする。

demo

開発環境

Ruby 2.5.1
Rails 5.2.4
React 16.13.1

参考

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

[Rails API][React]axiosを用いて"POST"したい!
https://teratail.com/questions/166335

Ruby on Rails+ReactでCRUDを実装してみた
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

MacにNode.jsをインストール
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

まずはRails側から実装開始

apiモードでrails newする

$ rails new neko -d mysql --api
// 「--api」→オプションをつける事でapiモードで作成
// 「-d mysql」→DBにはMySQLを利用

gem 'rack cors'をインストールとモデル・コントローラを作成

Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'# 注目! 29行目あたりにコメントアウトされているのでコメントアウトを外す
$ cd neko
// アプリ階層へ移動
neko $ bundle install
// rack-corsをbundle installする
neko $ rails g model post name:string neko_type:string
// モデルを作成する
neko $ rails db:create
neko $ rails db:migrate
neko $ rails g controller posts
// コントローラー作成。apiモードなのでviewは作成されない
posts_controller.rb
class PostsController < ApplicationController
  def index
    @post = Post.all
    render json: @post
  end

  def create
    @post = Post.create(name: params[:name], neko_type: params[:neko_type])
    render json: @post
  end

end

通常railsではprivate以下にpost_paramsなどメソッド作ってPost.create(post_params)としますが、Reactを使って実装すると以下のエラーが出てしまいました。

Started POST "/posts" for ::1 at 2020-04-06 20:37:54 +0900
Processing by ProductsController#create as HTML
  Parameters: {"name"=>"たま", "neko_type"=>"雑種"}
Completed 500 Internal Server Error in 5ms (ActiveRecord: 0.0ms)



NoMethodError (undefined method `permit' for "たま":String):

なので、今回は一旦@post = Post.create(name: params[:name], neko_type: params[:neko_type])で実装を進めていきます。

ルーティングを設定

routes.rb
Rails.application.routes.draw do
  resources :posts
end

Railsを起動

Reactが3000番ポート、Railsは3001番ポートを使います。

$ rails s -p 3001
=> Booting Puma
=> Rails 5.2.4.2 application starting in development 
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.4 (ruby 2.5.1-p57), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3001
* Use Ctrl-C to stop

// Getのエンドポイントにアクセス。
// ターミナルで新しいタブを開いて以下コマンドを叩いてみてjsonでレスポンスあるか確認
neko $ curl -G http://localhost:3001/posts/
=>[ ]
// 空の[]が返ってくるが、DBにデータが入っていないのでここはこのままでOK

Rails側で3000ポートのアクセスを許可する

config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors
8行目~16行目あたりにある以下のソースのコメントアウトを外す
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3000'# 注目! 3000番ポートを許可する為に'http://localhost:3000'に変更する

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

ここまででRails側の実装は終了です。次はReact側を実装していきます。

次にReact側の実装開始

Node.jsをまだインストールしていなければインストールして下さい。

neko $ npm install -g create-react-app
// create-react-appをインストール
neko $ create-react-app react_front
// プロジェクトを作成
neko $ cd react_front
react_front $ npm start
// reactのデフォルトTOP画面がブラウザに表示される

ディレクトリ構造

ディレクトリ構造は以下のようになります。

.
|_app
|_bin
|_config
|_db
|_lib
|_log
|_public
|_react_front # 注目 ここがこれから実装するディレクトリ
| |_node_modules
| |_public
| |_src
| |_.gitignore
| |_package-lock.json
| |_package.json
| |_yarn.lock
|_storage
|_test
|_tmp
|_vender
|_.gitignore
|_.ruby-version
|_config.ru
|_Gemfile
|_Gemfile.lock
|_package-lock.json
|_README.md

ちなみにgitでバージョン管理する場合にリモートリポジトリにpushする時にエラーが起きるかもしれません(エラー内容忘れた)。macのFinderでディレクトリに潜っていくと分かるのですが、Railsの.gitディレクトリとReactの.gitディレクトリが存在します。それぞれプロジェクト作成した時に作成されてしまうからです。

スクリーンショット 2020-04-07 18.47.18.png

キャプチャは既に片方削除していますが、React(react_front内)の.gitディレクトリは削除して下さい。それでエラーは解決できます。

App.js

コードはこちらを参考にカラム名やパスを自分の環境に合わせて変更して利用させて頂きました。

App.js
import React from 'react';
import { BrowserRouter as Router, Link, Switch, Route } from 'react-router-dom';
import List from './Components/List';
import New from './Components/New';
import './App.css';

const App = () => {
  return (
    <Router>
      <div id="App">
        <Header />
          <Switch>
            <Route exact path='/' component={ List }/>
            <Route exact path='/new' component={ New } />
          </Switch>
      </div>
    </Router>
  );
}
const Header = () => (
  <nav>
    <div>
      <Link to="/">SampleTodo</Link>
      <Link to="/new">新規投稿</Link>
    </div>
  </nav>
)

export default App;

Components/List.jsx

画面遷移を簡単に実装する為にaxiosをインストールしておきます。

neko $ npm install axios --save
// 「--save」オプションでpackage.jsonにも反映
List.jsx
import React, { Component } from 'react';
import axios from 'axios'

class List extends Component {
  constructor(props) {
    super(props)
    this.state = {
      posts: []
    };
  }

  componentDidMount() {
    axios.get('http://localhost:3001/posts')
    .then((results) => {
      this.setState({products: results.data})
    })
    .catch((data) =>{
      console.log(data)
    })
  }

  render() {
    const {posts} = this.state
    return (
      <div>
        {posts.map((list) => {
            return <li key={list.id}> { list.name }{ list.neko_type }</li>
            // postsに格納されているdataをmapメソッドを使い1つ1つ取り出し表示させる
          })}
      </div>
    );
  }
}
export default List;

Components/New.jsx

Components/New.jsx
import React, { Component } from 'react';
import axios from 'axios'

class New extends Component {
  constructor(props){
    super(props);
    this.state = {
      name: '',
      neko_style: ''
    };
  }

  handleInputValue = (event) => {
    this.setState({
    // setStateメソッドで更新するstateと新しいstateの値を指定する
      [event.target.name]: event.target.value
    // フォームのname="neko_type"のnameを参照
       // this.setState({title: event.target.value})と同じ書き方となる
    });
  }

  handleSubmit = (e) => {
    e.preventDefault();
    axios({
      method : "POST",
      url : "http://localhost:3001/posts",
      data : { name: this.state.name, neko_type: this.state.neko_type }
    })
    .then((response)=> {
      console.log(this.props)
      this.props.history.push('/');
    })
    .catch((error)=> {
      console.error(error);
    });
  }

  render() {
    const { name, neko_type } = this.state;
    return (
      <div>
        <p>新規投稿</p>
        <div>
          <label>名前 : </label>
          <input type="text" name="name" value={ name } onChange={ this.handleInputValue } />
          // 2つ以上のフォームを扱う場合はname=""を書く
      // これで1つのhandleInputValueイベントで複数のstateの値を更新できる
          <label>猫種 : </label>
          <input type='text' name="neko_type" value={ neko_type } onChange={ this.handleInputValue } />
      // 2つ以上のフォームを扱う場合はname=""を書く
      // これで1つのhandleInputValueイベントで複数のstateの値を更新できる
          <input type="button" onClick={this.handleSubmit} value="Submit" />
        </div>
      </div>
    );
  }
}


export default New;

まとめ

最初は1つのデータだけ送るフォームでCRUDを実装してキャッキャウフフしていましたが、フォームって複数のデータを扱えないと使えない事に気づき、色々な参考記事をかいつまみながら実装しました。
まだまだReactとRails APIの連携に対して理解力が足ないので、もっともっと勉強が必要と感じました。
次は残りの更新(update)削除(delete)を実装して開発中のアプリに載せ替えしようと思います。

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

rspecで例外が起きたことを確認する

この記事で書くこと :pen_ballpoint:

以下のような処理でのensureの処理が走ったかどうかを確認する方法

class Cat < ApplicationRecord
  validates :todays_foods, presence: true

  def sing!
    sing_a_song!
  ensure
    say_hello
  end

  def say_hello
    'にゃーん'
  end

  def sing_a_song!
    if todays_foods.empty?
      raise お腹が空いてたらエラー
      return
    end
    'にゃにゃにゃーん!'
  end
end

テストの中身

  describe '#sing!' do
     context 'with exception error,' do
       let!(:cat) do
         cat = build(:cat, name: 'お腹すいた猫', todays_foods: 0)
         cat.save(validate: false) # validatesを無効にして作成。
         cat
       end
       it 'say にゃーん.' do
         expect do
           cat.sing!
         end.to raise_error(ActiveRecord::RecordInvalid).and(eq('にゃーん'))
       end
     end
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クロスサイトスクリプティング(XXS)対策

クロスサイトスクリプティング(XXS)

Webアプリケーションでは、外部からの入力などに応じて表示が変化するページを実装したいことがしばしばあります。
しかし、この部分のHTML生成の実装に問題があると、外部よりスクリプトを埋め込まれクッキーを盗まれたり、JavaScriptによる攻撃を受けてしまうおそれがあります。
こういった攻撃手法をクロスサイトスクリプティングと言います。

XSSの攻撃例

では、実際にXSSの脆弱性を用いた攻撃例です。
ここでは登場人物を用いて説明します。
ユウスケ・・・ 一般ユーザ
タカシ ・・・攻撃を仕掛ける悪意のある者
1. タカシはXSSの脆弱性があるサイトに悪意のあるスクリプトを埋め込む
2. タカシは1.でスクリプトを埋め込んだサイトをリンクに指定する罠サイトを用意する
3. タカシはユウスケに罠サイトへ誘導するようなメールを送信する
4. ユウスケは罠サイトにアクセスし、タカシがスクリプトを埋め込んだサイトにアクセス
5. ユウスケのブラウザ上でタカシが埋め込んだスクリプトが実行される
※ここで出てくる罠サイトとは、スクリプトを埋め込んだXSSの脆弱性のあるサイトへのリンクを含んでいるページのことをいいます。
ここで実行されるスクリプトの例としては、cookieを攻撃者のサーバに送信されてしまったり、マルウェア(悪意のあるソフトウェア)を仕込んであるサイトにリダイレクトされウイルスに感染させられたりとJavascriptで記述することが可能なすべての攻撃を受けてしまう可能性があります。

XSSの対策

XSSが発生する主要因として、フォームから入力されたHTMLタグがそのままページに反映されてしまっていることがあげられます。
したがって、XSSを防ぐためにはHTMLを生成する際に意味を持つ「"」や「<」を文字参照によってエスケープすることが基本となります。

文字参照

HTML上で直接記述できない特殊文字を表記する際に用いられる記法です。例えば、HTML中に「<」もしくは「>」と記述するとこの二つはタグの初め、終わりと認識されてしまいます。これでは文字列として上記の記号を用いることができません。そこで、文字参照を利用します。

変換前 変換後
< & lt;
> & gt;
& & amp;
" & quot;
' & #39;

上の表は、XSSを対策する際にエスケープすべき特殊文字の一覧です。これらの特殊文字を文字参照に変換して保存すれば、外部から埋め込まれたスクリプトが実行されることはありません。

rawメソッド

文字列を文字参照にエスケープしないためのヘルパーメソッドです。
【例】

raw(文字列)
<%= raw(tweet.text) %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sidekiqのキューが処理されずCPU負荷が上がって困っている人へ

現象

  • CPU使用率が100%近くで張り付いてしまう
  • sidekiqのキューが処理されない

Compute_Engine_-_mixture_-_Google_Cloud_Platform.png
Sidekiq.png

原因

redis3系で運用しているシステムでsidekiqのバージョンを> 5.2.7から< 6.0に上げた為。
> 5.2.7まではredis2.8(3.0.3)以上であれば使えていたけど、< 6.0からは4以上が必須に。
(※筆者はmastodonv3.1.3にアップデートしたタイミングで発生)

解決策

redisのバージョンを4以上に上げてください

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

情報セキュリティとは

情報セキュリティ

Webサービスにおいてのセキュリティ(安全保障)です。情報セキュリティにおける理想は、「不正なアクセスや情報の漏洩を防ぎつつ、権限がある人は便利に利用できる」状態を維持することです。「機密性」「完全性」「可用性」の3つの要素を維持することを目標にします。
①機密性 権限を持たない人が情報資産を見たり使用できないようにすること
②完全性 権限を持たない人が情報を書き換えたり消したりできないようにすること
③可用性 権限を持つ人がいつでも利用したいときに利用できるようにすること

脆弱性(ぜいじゃくせい)について

コンピュータやネットワーク、アプリケーション全体のセキュリティに弱点を作り出すコンピュータソフトウェアの欠陥や仕様上の問題点のことを脆弱性と言います。Webアプリケーションに脆弱性があると、開発者側だけでなく、利用者側も被害を被る可能性があり、様々な被害が生じる可能性があります。脆弱性は、バグや、開発者のセキュリティチェック不足により生まれます。

脆弱性によってもたらされる被害

脆弱性がアプリケーション内に存在することによって、以下のような被害が想定されます。
①個人情報を勝手に閲覧される(機密性侵害)
②Webページの内容が改ざんされる(完全性侵害)
③Webページ自体が利用不能になる(可用性侵害)
このような問題が起きてしまうと、利用者への金銭的損失の補填や補償、開発者の社会的信頼の失墜による売上の減少、Webサイト停止による機会損失など多くの被害をもたらすので脆弱性対策は必須です。

脆弱性が生まれる理由

一つは、バグによるもの
二つ目は、開発者側のセキュリティチェック不足によるもの
ここでいうバグとは、クロスサイトスクリプティングのように投稿フォーム等にscriptタグでJavascriptのコードを記述・送信することで、それがページに埋め込まれ、実行されてしまうといったようなプログラミング言語の仕様に起因するものようなことを言います。

クロスサイトスクリプティング(XSS)

Webサイトに利用されるアプリケーションの脆弱性もしくはその脆弱性を悪用した攻撃のことです。特にWeb閲覧者側が制作することのできる動的サイト(例:TwitterなどのSNS、掲示板等)に対して、その脆弱性を利用して悪意のある不正なスクリプトを挿入することによりその発生するサイバー攻撃です。

HTTP

HTTPとはWebブラウザとWebサーバの間でHTMLや画像ファイルなどのコンテンツの送受信に用いられる通信プロトコルです。Webページを閲覧・利用することができるのもHTTPという仕組みがあるからです。

プロトコル

複数のユーザが滞りなく信号やデータ、情報を相互に伝送できるよう、あらかじめ決められた約束事や手順の集合のことです。
例えば人と人との会話を例とすると、片方が日本語で話し、もう片方が中国語で話していては会話にならない状態です。
そこで、会話を成立させるためには使用言語をきちんと決める必要があります。この使用言語を何にするか決める役割が通信におけるプロトコルにあたります。
HTTPはクライアント(自分のパソコンなど)、ブラウザからのリクエストに対して、サーバからのレスポンスが返ってくることによって実現します。
例えば飲食店で考えたときに、料理を注文します。すると、お店側は注文された料理を提供します。
HTTPもこれと同じでクライアントが要求したページをサーバ側がクライアントに合わせて提供するという仕組みになっています。
例として、クライアントとサーバー間でHTTP通信を行う際はメッセージのやり取りを行います。
まず、クライアントからサーバーに対して以下のようなリクエストメッセージを送信します。

HTTPリクエスト
GET / HTTP/1.1 リクエストライン
Accept: image/gif, image/jpeg, / ヘッダ
Accept-Language: ja
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (Compatible; MSIE 6.0; Windows NT 5.1;)
Host: www.xxx.zzz
Connection: Keep-Alive
(空行)
メッセージボディ(POSTメソッドなどで使用)

www.xxx.zzz というアドレス
に対してHTTP/1.1というバージョンを使い、GETメソッドで"/"パスにアクセスしたい、というメッセージをクライアントからサーバに送っているいう意味になります。
それに対してサーバは以下のようなレスポンスメッセージを返します。

HTTPレスポンス
HTTP/1.1 200 OK ステータスライン
Date: Sun, 11 Jan 2004 16:06:23 GMT ヘッダ
Server: Apache/1.3.22 (Unix) (Red-Hat/Linux)
Last-Modified: Sun, 07 Dec 2003 12:34:18 GMT
ETag: "1dba6-131b-3fd31e4a"
Accept-Ranges: bytes
Content-Length: 4891
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html
(空行)
htmlやcssの情報(メッセージボディ)

ステータスラインを見てみると先ほどのリクエストメッセージに対して、HTTP1.1/(バージョン)200(ステータス番号)OK(補足メッセージ)と返しています。
これは「HTTP/1.1というバージョン下でリクエストを受け付けました」という意味を表しています。

ステータスコード

HTTP通信のレスポンスメッセージのステータスライン中にある3桁の数字で、クライアントからのリクエストに対して、サーバからの返
100の位に意味があり、そちらで分類されます。

ステータスコード 意味
100番台 処理が継続中
200番台 正常終了
300番台 リダイレクト
400番台 クライアント側でのエラー
500番台 サーバー側でのエラー

よく使われるステータスコードとしては、200(正常終了)、301及び302(リダイレクト)、404(ファイルが存在しない)、500(内部サーバーエラー)などがあります。

セッション/クッキー(cookie)

HTTPは、ステートレスな通信となっています。
ステートレスとはサーバが現在の状態を保持せず、ユーザの入力の内容のみによって出力が決定される状態のことです。
例えば、ユーザのログイン状態やECサイトのカート機能などがあります。これらは、ページが遷移してもユーザの情報や購入しようとしている商品の情報を保持している必要があります。
こういった状態を保持するために用いられるのがセッションであり、HTTP通信のセッションを管理するために作られた仕組みをクッキー(cookie)といいます。このような通信をステートフル通信といいます。

セッション

複数回に渡るリクエストにおいて、クライアントを特定するための仕組みです。具体的には、クライアントは初回のリクエストで自身を識別させるIDをサーバーに渡します。以降、サーバーはそのIDを持ってクライアントを認識します。

クッキー(cookie)

クライアント側のブラウザに保持することができる情報のことです。通常、初回の通信でサーバーがクライアントにクッキーとしてセッションIDを保持させ、以降クライアントはそれを用いてサーバーに対して自身を特定させます。
実際にクッキー(cookie)がどのように使われているか
まず、サーバー側で明示的にクッキー(cookie)を設定しますよ、という宣言をします。
Railsでは、session_store.rbというファイル内でセッションの管理方法を指定します。デフォルトでクッキー(cookie)を利用する設定になっているため、Railsでは意識せずクッキー(cookie)でのセッション管理を行うことができます。

config/initializer/session_store.rb
TechReviewSite::Application.config.session_store :cookie_store, key: '_tech_review_site_session'

ログイン処理を例にとると、以下のような流れでクッキー(cookie)が利用されます。
①クライアントはログイン画面でIDとパスワードを入力する
②すると、サーバでクッキー(cookie)が生成され、クライアントが保持する
③次回以降アクセスする際に、クライアントが保持しているクッキー(cookie)がサーバに送信される
④サーバはこのクッキー(cookie)値を元にクライアントを識別し、ログイン作業を省く
まとめると
HTTPとはWebアプリケーションを利用する際にクライアントとサーバ間の情報をやり取りするための通信プロトコル
セッションとは、Webアプリケーション上で前のページの状態を保持するために利用される機能

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

PostgreSQLのデータベースの存在を確認する

はじめに

Railsのコマンドから、PostgreSQLにデータベースを作成/削除をして、本当に作成されているか、削除されているか、確認してみる。

環境

Vagrant + Ubuntu 16.04.5 LTS
Rails 5.2.4.2

手順

Railsのプロジェクトファイルを作成する。

$rails new test01 -d postgresql -B
$cd test01

データベースを作成する。

$rails db:create
Created database 'test01_development'
Created database 'test01_test'

データベースの一覧を表示する。

psql -l

確かに、作成されている事が確認できます。一覧は、qキーで閉じる事ができます。

image.png

<参考>
https://db.just4fun.biz/?PostgreSQL/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E4%B8%80%E8%A6%A7%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B%E6%96%B9%E6%B3%95

次は、一旦作成したデータベースを削除します。

$ rails db:drop
Dropped database 'test01_development'
Dropped database 'test01_test'

psql -lで確かに、削除された事が確認できます。

再度、データベースを作成して、マイグレーションのバージョンを確認すると、まだ、マイグレーションは1度も実行していないため、バージョンは0で表示される筈です。

$rails db:create
$rails db:version
Current version: 0

適当に、Usersテーブルを作成する。

$rails g model Users name:string
$rails db:migrate

Usersテーブルが確かに作成されている事を確認する。

$psql -q -c 'select * from Users' test01_development
 id | name | created_at | updated_at
----+------+------------+------------
(0 rows)

マイグレーションをロールバックすると、先ほど作成したUserテーブルは削除されるため、テーブルを参照するとテーブルは存在しませんと言われ、エラーになります。

<補足>
psqlのcオプションは、SQLコマンドを投げるためのコマンドです。詳しくは、psql --helpでコマンドの詳細が確認できます。

$rails db:rollback
$psql -q -c 'select * from Users' test01_development
ERROR:  relation "users" does not exist
LINE 1: select * from Users
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Ajaxを用いた非同期投稿の実装

目標

ezgif.com-video-to-gif (1).gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

ログイン機能を実装済み。

ログイン機能 ➡︎ https://qiita.com/matsubishi5/items/5bd8fdd45af955cf137d

投稿機能の実装

テーブル

schema.rb
ActiveRecord::Schema.define(version: 2020_04_05_115005) do

    create_table "books", force: :cascade do |t|
        t.string "title"
        t.text "body"
        t.integer "user_id"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false
    end

    create_table "users", force: :cascade do |t|
        t.string "email", default: "", null: false
        t.string "encrypted_password", default: "", null: false
        t.string "reset_password_token"
        t.datetime "reset_password_sent_at"
        t.datetime "remember_created_at"
        t.integer "sign_in_count", default: 0, null: false
        t.datetime "current_sign_in_at"
        t.datetime "last_sign_in_at"
        t.string "current_sign_in_ip"
        t.string "last_sign_in_ip"
        t.string "name"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false

        t.index ["email"], name: "index_users_on_email", unique: true
        t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    end
end

モデル

user.rb
class User < ApplicationRecord
    has_many :books, dependent: :destroy
end
book.rb
class Book < ApplicationRecord
    belongs_to :user
end

ルーティング

routes.rb
Rails.application.routes.draw do
    devise_for :users
    resources :users
    resources :books
end

コントローラー

books.controll.rb
class BooksController < ApplicationController
    def create
        book = Book.new(book_params)
        book.user_id = current_user.id
        if book.save
            redirect_to book, notice: "successfully created book!"
        else
            @books = Book.all.order(created_at: :desc) #降順
            render 'index'
        end
    end

  def index
    @book = Book.new
    @books = Book.all.order(created_at: :desc) #降順
  end

    private
        def book_params
            params.require(:book).permit(:title, :body)
        end
end

ビュー

books/index.html.erb
<% if @book.errors.any? %>
    <h2><%= @book.errors.count %>error</h2>
    <div>
        <ul>
            <% @book.errors.full_messages.each do |msg| %>
                <li><%= msg %></li>
            <% end %>
        </ul>
    </div>
<% end %>

<%= form_with model: @book do |f| %>
    <div class="field row">
            <%= f.label :title %>
            <%= f.text_field :title, class: "col-xs-12 book_title" %>
    </div>

    <div class="field row">
        <%= f.label :body %>
        <%= f.text_area :body, class: "col-xs-12 book_body" %>
    </div>

    <div class="actions row">
            <%= f.submit class: "btn btn-primary col-xs-12" %>
    </div>
<% end %>
<h2>Books index</h2>
<table class="table table-hover table-inverse">
    <thead>
        <tr>
            <th></th>
            <th>Title</th>
            <th>Opinion</th>
        </tr>
    </thead>
    <tbody>
        <% @books.each do |book| %>
            <tr>
                <td>
                    <%= link_to book.user do %>
                         <%= attachment_image_tag book.user, :profile_image, :fill, 50, 50, fallback: "no-image-mini.jpg" %>
                    <% end %>
               </td>
                <td><%= link_to book.title, book %></td>
                <td><%= book.body %></td>
            </tr>
        <% end %>
    </tbody>
</table>

非同期機能の実装

1. jQueryの導入

Gemfile
    gem 'jquery-rails'
ターミナル
    $ bundle
application.js
    //= require rails-ujs
    //= require activestorage
    //= require turbolinks
    //= require jquery
    //= require_tree .

2. booksコントローラーのcreateアクションを編集

books.controller.rb
class BooksController < ApplicationController
#ローカル変数をインスタンス変数に変更
    def create
        @book = Book.new(book_params)
        @book.user_id = current_user.id
        unless @book.save
            render 'index'
        end
    end
end

3. フォームを編集する

非同期投稿を行うときは必ず「form_with」を使用する。

※「form_for」「form_tag」だとレイアウトが崩れた。

books/index.html.erb
<!-- form_withの引数に「remote: true」を追記 -->
<%= form_with model: @book, remote: true do |f| %> 
    <div class="field row"> 
            <%= f.label :title %>
            <%= f.text_field :title, class: "col-xs-12 book_title" %>
    </div>

    <div class="field row">
        <%= f.label :body %>
        <%= f.text_area :body, class: "col-xs-12 book_body" %>
    </div>

    <div class="actions row">
            <%= f.submit class: "btn btn-primary col-xs-12" %>
    </div>
<% end %>

4. 投稿一覧を編集する

books/index.html.erb
<tbody class="new_book"> <!-- 投稿一覧の親要素にクラスをつける -->
    <% @books.each do |book| %>
        <%= render 'books', book: book %> <!-- 投稿一覧をパーシャル化 -->
    <% end %>
</tbody>

books/_books.html.erb
<tr>
    <td>
        <%= link_to book.user  do %>
            <%= attachment_image_tag book.user, :profile_image, :fill, 50, 50, fallback: "no-image-mini.jpg" %>
        <% end %>
    </td>
    <td><%= link_to book.title, book %></td>
    <td><%= book.body %></td>
</tr>

5. JavaScriptファイルの作成

books/create.js.erb
$(".book_title").val('');
$(".book_body").val('');
$(".new_book").prepend("<%= j(render 'books', book: @book) %>");

$(".new_book")
➡︎ 「4」で付けたクラスを指定

.prepend("<%= j(render 'books', book: @book) %>");
➡︎ 既存投稿の先頭に新規投稿を表示

$(".book_title").val('');
$(".book_body").val('');
➡︎ 入力フォームを空にする

参考サイト

https://qiita.com/hiro266/items/56ec2c350fd9d8ca22d8

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

RailsでERBを直接使い、文字列として結果を取得する

はじめに

Railsのviewではなく、erbの結果を文字列としてほしいケースが有ったので、その方法です。

パスにRails固有のコードを使っていますが、そこ以外はRailsじゃなくても使えます。

ERBのインスタンス生成

file = File.read(Rails.root.join('app', 'views', 'hoge', 'fuga.html.erb').to_s)
erb = ERB.new(file)

といった形でERBのインスタンスを生成できます。

Railsじゃない場合は、

file = File.read('/path/to')

みたいな感じですね。

結果の取得

result_text = erb.result_with_hash(hoge:1, fuga:2)

で結果の取得ができます。

引数に渡したハッシュが、そのままerb内でkeyを変数名、valueを値とするローカル変数として読み込まれます。

あとがき

erbを直接使いたい状況、そこまで多くはないとは思いますが、もし必要な方の役に立てば幸いです。

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

【Rails5】rails_adminで多対多の関連付けを上手く編集する方法

はじめに

一瞬でめちゃイケてる管理画面を作ってくれるrails_admin
今までCRUDそれぞれ画面編集してたのはなんだったんだ……と思う完成度です。

しかし、多対多の関連付けの時、毎回のようにエラーが出ました。

出たエラー

ActiveRecord::HasManyThroughOrderError in RailsAdmin::Main#edit

原因

色々試しましたが、Modelでの関連付け設定の順番が原因でした。

構成

タイトルごとに複数のタグ。
タグごとに複数のタイトル。

title has_many tags
tag has_many titles

これを実現させるために、tag_mapsというテーブルを間に挟んでいます。

[tag_maps]
id
title_id
tag_id

解決したコード

model/title.rb
class Title < ApplicationRecord
  has_many :tag_maps
  has_many :tags, through: :tag_maps
end
model/tag_map.rb
class TagMap < ApplicationRecord
  belongs_to :title
  belongs_to :tag
end
model/tag.rb
class Tag < ApplicationRecord
  has_many :tag_maps
  has_many :titles, through: :tag_maps
end

ただ普通に記述してるだけなのですが、なぜか順番が逆だとエラーが出ました。

オマケの注意点

今回のこれは、Rails5での解決策でした。
(Rails 5.2.4.2)

他のバージョンでは、ここ見たら参考になるかも……?
https://github.com/sferik/rails_admin/wiki/Associations-basics

あとrails_adminの編集画面で若干表示項目が変に見えるのは……まぁええでしょ。エラー出てないし

終わり

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

[Rails]モデル、テーブル、カラムの作成と削除コマンド

本記事について

開発して行く中で
「あれ?マイグレーションファイルの作成コマンドってこんなんだっけ。」
のように、ド忘れしてしまうことがあります。
そのため、メモ代わりに書きたいと思います。

DB関係

モデル & テーブル作成

モデル & マイグレージョンファイル作成

rails g model モデル名(単数)

例 : userモデル
rails g model user

補足ですが上のコマンドで以下が作成されます。
1. モデルのクラスファイル
2. マイグレーションファイル
3. モデルの自動テスト
4. モデルの自動テストで使うfictureファイル

実行

bundle install

モデル削除

rails d model モデル名(単数)

テーブルの削除

主な流れ

- マイグレーションファイル作成
- マイグレーションを編集
- 実行

例: users テーブルの削除
マイグレージョンファイルの作成

rails g migration DropTableUsers

マイグレーションファイルの編集

2020xxxxx.rb
class DropTableUsers < ActiveRecord::Migration

  def change
    drop_table :users
  end

end

実行

bundle install

既存のテーブルにカラムを追加

rails g Addカラム名Toテーブル名 カラム名:カラム型
の形でターミナルに入力しマイグレーションファイルを作成します。

例: usersテーブルにstring型nameカラムを追加

rails g migration AddNameToUsers name:string

実行

bundle install

既存のテーブルのカラムを削除

rails g migration Removeカラム名Fromテーブル名 カラム名:テーブル名
の形でターミナルに入力しマイグレーションファイルを作成します。

例: usersテーブルのstring型nameカラムを削除

rails g migration RemoveNameFromUsers name:string

実行

bundle install

おわり

最後まで見ていただきありがとうございました。

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

【Rails】マイグレーションファイルやschemaファイルの仕組み、rails db:migrate、rails db:rollbackとかを纏めてみた

マイグレーションファイルとは?

マイグレーションファイルとはデータベースの設計図のことです。

このマイグレーションファイルをどのように作成して、データベースに反映させるのか、
また、一度データベースに反映させた内容をどのように修正できるのか、自分なりに纏めてみました。

マイグレーションファイルを作成し、データベースに反映させる

まずはターミナルでマイグレーションファイル(DBの設計図)を作成します。
僕の場合、text型のbodyカラムを持つ、BoardモデルとUserモデのマイグレーションファイルを作成しました。

まずはreference型にboard_id:referencesuser_id:referencesと間違った内容を付けたとします。

# 誤った内容であることに注意!
rails g model comment body:text board_id:references user_id:references

これによって、以下のマイグレーションファイルが作成される。

db/migrate/20200330045356_create_comments.rb
# 誤ったファイルであることに注意!
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.text :body, null: false
      t.references :board_id, foreign_key: true
      t.references :user_id, foreign_key: true

      t.timestamps
    end
  end
end

このマイグレーションファイルを作成した段階では、データベースに反映されていません。
ターミナルで下記のrails db:migrateコマンドを実行すると、作成したマイグレーションファイルが読み込まれ、データベースに反映されます。

> rails db:migrate
== 20200330045356 CreateComments: migrating ===================================
-- create_table(:comments)
   -> 0.0143s
== 20200330045356 CreateComments: migrated (0.0165s) ==========================

データベースに反映されているマイグレーションファイルを確認する

そして、rails db:migrate:statusを実行すると、DBに反映されたマイグレーションファイルを確認できます。
upと書いているファイルがデータベースに反映されているもの(マイグレーション済みということ)です。

> rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200310093526  Sorcery core
   up     20200316101344  Create boards
   up     20200316105948  Add user id to boards
   up     20200328064702  Add board image to board
   up     20200330045356  Create comments

次に、スキーマファイルで現在のデータベースの構造を確認できるのですが、
下記のcommentsテーブルはboard_id_iduser_id_idと誤ったカラムが追加されているので、修正したいです。

db/schema.rb
# 誤ったテーブル内容
create_table "comments", force: :cascade do |t|
    t.text "body", null: false
    t.integer "board_id_id"
    t.integer "user_id_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["board_id_id"], name: "index_comments_on_board_id_id"
    t.index ["user_id_id"], name: "index_comments_on_user_id_id"
  end

データベース及びマイグレーションファイルの修正方法

まずは、rails db:rollbackで最新のマイグレーションファイルをdown状態にします。
down状態のマイグレーションファイルは、データベースに反映されていない状態にあるということです。
(up状態にあるマイグレーションファイルを削除・編集することは避けましょう)

> rails db:rollback

== 20200330045356 CreateComments: reverting ===================================
-- drop_table(:comments)
   -> 0.0044s
== 20200330045356 CreateComments: reverted (0.0079s) ==========================

rails db:migrate:statusで、以下の様にdown状態になったことが確認できますね。

> rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200310093526  Sorcery core
   up     20200316101344  Create boards
   up     20200316105948  Add user id to boards
   up     20200328064702  Add board image to board
  down    20200330045356  Create comments

次に行う手順として、2通り方法があります。
①down状態にしたマイグレーションファイルを削除し、新しいマイグレーションファイルを作成してから、rails db:migrateする。
②down状態にしたマイグレーションファイルを直接編集し、rails db:migrateする。

どちらでもいいのですが、今回はカラム名を修正するだけなので、②の方法を取ってみます。
board_idboardに、user_iduserに変更します。

db/migrate/20200330045356_create_comments.rb
# 誤ったファイルであることに注意!
class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.text :body, null: false
      t.references :board, foreign_key: true
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

これでrails db:migrateすると、マイグレーションファイルがup状態に戻ります。

> rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200310093526  Sorcery core
   up     20200316101344  Create boards
   up     20200316105948  Add user id to boards
   up     20200328064702  Add board image to board
   up     20200330045356  Create comments

スキーマファイルを確認すると、

db/schema.rb
create_table "comments", force: :cascade do |t|
    t.text "body", null: false
    t.integer "board_id"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["board_id"], name: "index_comments_on_board_id"
    t.index ["user_id"], name: "index_comments_on_user_id"
  end

無事、正しいカラム名を持ったテーブルがデータベースに反映されました!

まとめ

最後に、ここまでの内容をまとめておきます!

  • マイグレーションファイルとはデータベースの設計図のこと。
  • rails g model モデル名 カラム名:型で、モデルとマイグレーションファイルを作成する
  • rails db:migrateで、マイグレーションファイルをDBに反映させる(up状態)
  • rails db:rollbackで、マイグレーションファイルをDBに反映させる前の状態に戻す(down状態)
  • rails db:migrate:statusで、各マイグレーションファイルのDBへの反映状態(up,down)を確認できる
  • マイグレーションファイルの修正方法は、①down状態にしたマイグレーションファイルを削除し、新しいマイグレーションファイルを作成してから、rails db:migrateする。もしくは、 ②down状態にしたマイグレーションファイルを直接編集し、rails db:migrateする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sign in with apple実装中に、サーバーサイド側でJWTを検証しようとしたところ、Signature verification raisedが発生する

iOSクライアントからJwt送信 -> Rails側でデコードをする場合に Signature verification raisedが発生した
各種パラメーターは正しく与えているのにデコードに失敗してしまう
jwtの有効期限が切れたのかと思ったがそういうわけでもないようだった
結論、keysの正しいkidを選択していなかったことだった
Appleのサイトから証明書のjsonを取得すると、keysが2つあるのに気づいていた
確認したところ、デコードに使えるのは片方1つのみで、それはjwtの中に指定されている
正しいkeyのkidを指定したところ、うまく動くようになった

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

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

メモの目次記事はこちら

公式Railsチュートリアル第14章へのリンク

サマリ

  • モデルの複雑な関連付けの方法
    • 1つのテーブルrelationshipを使って、followerfollowingを管理する方法
  • ネストしたルーティングの実装
  • ajaxの実装

ポイント

テーブル名と異なるカラムをFKとして使用したい場合

1対多の1側

  # user.rb
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy

1対多の多側

  # relationship.rb
  # follower_idカラムが存在している必要がある
  belongs_to :follower, class_name: "User"

has_manybelongs_toについて

has_manyで指定できるパターン

  # モデルの複数形 e.g. microposts
  has_many :microposts
  # 別名のシンボル、クラス名、FK
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id"
  • 関連付けとは別に、has_many :hoge, through: :fuga, source: :foobarを定義しておくと、こ配列``

belongs_toで指定できるパターン

デフォルトでは外部キーの名前を_idといったパターンとして理解し、 に当たる部分からクラス名を推測する
1. モデルのシンボル
2. 別名とクラス名

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

ルーティングのカスタマイズ - ネストしたURLの設定

  # /users/1/following や /users/1/followers 
  resources :users do
    member do
      get :following, :followers
    end
  end

  # /users/tigers
 resources :users do
    collection do
      get :tigers
    end
  end

Ajaxの実装方法

  • ローカル変数ではなくインスタンス変数への変更が必要
  • viewにはform_forの引数に, remote: trueを追加する
  • controllerにはrespond_toを追加する
  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

アンパサンド(&)を使ったブロックの短縮表記

# 以下は同値
[1, 2, 3, 4].map { |i| i.to_s }

[1, 2, 3, 4].map(&:to_s)

感想

  • めちゃくちゃ時間がかかったがなんとか完走することができた
  • チュートリアルとはいえtwitter風の動くアプリが完成したのは嬉しい
  • まだまだ理解しきったといえる状態ではないので、3周目4週目をやりながら使いこなしていきたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.14 - TDDでPost機能をコーディング part3 -

はじめに

こんにちは!
またまたPost機能の開発の続きです。今回でラスト!

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

前回の残り

  1. 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  2. 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  3. サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  4. サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  5. サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
  6. サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

残り6シナリオ。ユーザー詳細ページにそのユーザーのポストを表示する機能ですね。

では早速最後のコーディングをしていきましょう!!

未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること

ユーザー詳細ページでそのユーザーのポストが投稿日時降順で表示されていることと、他のユーザーのポストが表示されていないことを検証します。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること" do
+     # テスト用のユーザーを作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
+     # ポストを用意する
+     posts1 = []
+     posts1.unshift Post.create(content: "First Post!!", user: user1)
+     posts1.unshift Post.create(content: "Second Post!!", user: user1)
+     posts2 = []
+     posts2.unshift Post.create(content: "初めてのポスト", user: user2)
+     posts2.unshift Post.create(content: "2回目のポスト", user: user2)
+
+     # user1のユーザー詳細ページにアクセスする
+     visit user_path(user1)
+
+     # user1のポストが投稿日時降順で表示されていることを検証する
+     posts1.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+     end
+     # user2のポストは表示されないことを検証する
+     posts2.each do |post|
+       expect(page).not_to have_text post.user.name
+       expect(page).not_to have_text post.content
+     end
+
+     # user2のユーザー詳細ページにアクセスする
+     visit user_path(user2)
+
+     # user2のポストが投稿日時降順で表示されていることを検証する
+     posts2.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+     end
+     # user1のポストは表示されないことを検証する
+     posts1.each do |post|
+       expect(page).not_to have_text post.user.name
+       expect(page).not_to have_text post.content
+     end
+   end        
  end

少し長いですが、今までの延長で理解できるコードになっているはずです!(コメントアウトも参考にしてね。)
さて、このテストを回してみましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
     Failure/Error: expect(find("#posts_list").all(".post-item")[i].find(".post-user-name")).to have_text user.name

     Capybara::ElementNotFound:
       Unable to find css "#posts_list"

Finished in 27.77 seconds (files took 5.67 seconds to load)
16 examples, 1 failure

この時点では#posts_listがないと怒られます。posts_list、つまりユーザー詳細ページでポストを表示する機能をコーディングしていないので、テストが失敗しています。

さて、今回のposts_listですが、ポストページで同じようにポストの一覧を表示するViewを作りました。開発を効率的に進めるために、是非その時の機能を利用したいですね。
Railsでは部分テンプレート(Partial Template)という機能があります。複数のテンプレートから呼び出されるような一部分のViewを別ファイルに切り出して、各テンプレートからそれを呼び出すようなイメージです。
百聞は一見にしかずですので、まずは試してみましょう。
まずは、ポストページのposts_list配下の要素を部分テンプレート化してみます。

# touch app/views/posts/_posts_list.html.erb

部分テンプレートは頭に_をつけるのが習わしです。

app/views/posts/_posts_list.html.erb
<% posts.each do |post| %>
  <div class="card post-item my-1">
    <div class="card-body">
      <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
      <p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>
    </div>
  </div>
<% end %>

部分テンプレートはこんな感じで書きます。posts/index.html.erbに書いていた内容と変わらないです。唯一変わるポイントは最初のeachする配列の変数が@postsからpostsになっていることです。
部分テンプレートは別のテンプレートファイルから呼び出されますが、その時にインスタンス変数でなくても変数を渡すことができます。これも実際にみてみた方が早いと思いますので、まずはposts/index.html.erbからこの部分テンプレートを読み込んで今と変わらない状態になることを確認してみましょう。

app/views/posts/index.html.erb
  ...
  <div id="posts_list" class="my-5">
-   <% @posts.each do |post| %>
-     <div class="card post-item my-1">
-       <div class="card-body">
-         <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
-         <p class="card-text"><%= safe_join(post.content.split("\n"), tag(:br)) %></p>
-       </div>
-     </div>
-   <% end %>
+   <%= render partial: "posts_list", locals: { posts: @posts } %>
  </div>
  ...

呼び出し方はrender partial:に対して適用したいテンプレートのファイル名(頭の_は除く)を指定するだけです。さらにオプションでlocals:の後に{ 変数名: 値 }をつけることで部分テンプレートに変数を受け渡すことができます。今回の例では@postsを部分テンプレート内のpostsに代入させていることになります。
変数は複数受け渡すことができ、その場合は{ 変数名1: 値1, 変数名2: 値2 }のように,で区切るだけです。

ここで一度デグレが起きていないかテストを実行しておきましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
     Failure/Error: expect(find("#posts_list").all(".post-item")[i].find(".post-user-name")).to have_text user.name

     Capybara::ElementNotFound:
       Unable to find css "#posts_list"

Finished in 37.59 seconds (files took 6.58 seconds to load)
16 examples, 1 failure

今取り掛かっているシナリオのテスト失敗だけなので、ポストページに関するテスト失敗は起きていませんね。うまく部分テンプレートが機能しているようです。

さて、ユーザー詳細ページでもこの部分テンプレートを利用しましょう。
ユーザー詳細ページではそのユーザーのポストだけを表示したいので、部分テンプレートに渡すposts変数にはそのユーザーのポストのArrayを渡してあげればいいことになります。

app/views/users/show.html.erb
  <div class="container my-5">
    <% flash.each do |msg_type, msg| %>
      <div class="alert alert-<%= msg_type %>"><%= msg %></div>
    <% end %>
    <%= @user.name %>
    <br>
    <%= @user.email %>
+
+   <div id="posts_list" class="my-5">
+     <%= render partial: "posts/posts_list", locals: { posts: @user.posts.order(created_at: :desc) } %>
+   </div>
  </div>

先ほどと少し違うのは、_posts_list.html.erbがこのファイルとは別のディレクトリ(posts/)にあるので、そのディレクトリも含めて部分テンプレートファイル名を指定しています。(posts/posts_list
また、posts変数には@user.posts.order(created_at: :desc)でそのユーザーのポストを作成日時降順で取得したArrayを部分テンプレートに渡しています。

ではテストを回してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 50.91 seconds (files took 7.01 seconds to load)
16 examples, 0 failures

Greenになりました!

未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

ポストページの場合はユーザー名クリックでユーザー詳細ページへ遷移させていましたが、ユーザー詳細ページ上では

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと" do
+     # テスト用のユーザーを作成する
+     user = create_user(1)
+     # テスト用のポストを作成する
+     posts = []
+     posts.unshift Post.create(content: "First Post!!", user: user)
+     posts.unshift Post.create(content: "Second Post!!", user: user) 
+
+     # userのユーザー詳細ページにアクセスする
+     visit user_path(user)
+
+     # ポストのユーザー名がリンクになっていないことを検証する
+     posts.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).not_to have_selector("a.post-user-name")
+     end
+   end
  end

ポストページと同じ部分テンプレートを使っているので、現在はポストのカードの中のユーザーの名前が表示されている要素はpost-user-nameをclass属性に付与されているaタグになっています。
これがリンクを作っているところなので、この要素がない状態であれば、ユーザー名をクリックしても何も起こらないことを検証できます。

# rspec spec/system/07_posts_spec.rb
Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
     Failure/Error: expect(find("#posts_list")).not_to have_selector("a.post-user-name")
       expected not to find visible css "a.post-user-name" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[1]/DIV[1]">, found 2 matches: "John Smith", "John Smith"

Finished in 41.18 seconds (files took 5.05 seconds to load)
17 examples, 1 failure

現在はリンクがついてしまっているのでこれをなんとかします。

link_toを使ってリンクを作っていますが、link_to_unless_currentを使ってみます。使い方はlink_toと変わりないのですが、リンク先が今のパスの場合は単なるテキストを表示してくれるメソッドです。

app/views/posts/_posts_list.erb
- <h5 class="card-title"><%= link_to post.user.name, post.user, class: "post-user-name" %></h5>
+ <h5 class="card-title"><%= link_to_unless_current post.user.name, post.user, class: "post-user-name" %></h5>

これでテストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 34.46 seconds (files took 4.86 seconds to load)
17 examples, 0 failures

無事テストがパスしました。ポストページの方もデグレは起きていないかも全てのテストをパスしていることから確認できますね。

サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること

上の2つのテストのサインイン済版ですね。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること" do
+     # テスト用のユーザーを作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
+     # ポストを用意する
+     posts1 = []
+     posts1.unshift Post.create(content: "First Post!!", user: user1)
+     posts1.unshift Post.create(content: "Second Post!!", user: user1)
+     posts2 = []
+     posts2.unshift Post.create(content: "初めてのポスト", user: user2)
+     posts2.unshift Post.create(content: "2回目のポスト", user: user2)
+     # user1でサインインする
+     sign_in(user1)
+
+     # user2のユーザー詳細ページにアクセスする
+     visit user_path(user2)
+
+     # user2のポストが投稿日時降順で表示されていることを検証する
+     posts2.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+     end
+     # user1のポストは表示されないことを検証する
+     posts1.each do |post|
+       expect(page).not_to have_text post.user.name
+       expect(page).not_to have_text post.content
+     end
+   end
  end

未サインインの時と検証内容は同じですね。

# rspec spec/system/07_posts_spec.rb

Finished in 36.67 seconds (files took 7.58 seconds to load)
18 examples, 0 failures

すでに実装済みですのでテストはGreenです。

サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

これも未サインインで同じテストをしているのでそれをパクります。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと" do
+     # テスト用のユーザーを作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
+     # テスト用のポストを作成する
+     posts = []
+     posts.unshift Post.create(content: "First Post!!", user: user2)
+     posts.unshift Post.create(content: "Second Post!!", user: user2) 
+     # user1でサインインする
+     sign_in(user1)
+
+     # user2のユーザー詳細ページにアクセスする
+     visit user_path(user2)
+
+     # ポストのユーザー名がリンクになっていないことを検証する
+     posts.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).not_to have_selector("a.post-user-name")
+     end
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 41.16 seconds (files took 5.61 seconds to load)
19 examples, 0 failures

これも実装済みなのでテストがパスしていますね。

サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること

これも以前のテストとほぼ同じ。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること" do
+     # テスト用のユーザーを作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
+     # ポストを用意する
+     posts1 = []
+     posts1.unshift Post.create(content: "First Post!!", user: user1)
+     posts1.unshift Post.create(content: "Second Post!!", user: user1)
+     posts2 = []
+     posts2.unshift Post.create(content: "初めてのポスト", user: user2)
+     posts2.unshift Post.create(content: "2回目のポスト", user: user2)
+     # user1でサインインする
+     sign_in(user1)
+
+     # user1のプロフィールページにアクセスする
+     visit user_path(user1)
+
+     # user1のポストが投稿日時降順で表示されていることを検証する
+     posts1.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.user.name
+       expect(find("#posts_list").all(".post-item")[i]).to have_text post.content
+     end
+     # user2のポストは表示されないことを検証する
+     posts2.each do |post|
+       expect(page).not_to have_text post.user.name
+       expect(page).not_to have_text post.content
+     end
+   end
  end

ほい。ではテストを回しましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 41.83 seconds (files took 6.77 seconds to load)
20 examples, 0 failures

これもパス。

サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

これも同じようなテストをすでにしていますね。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと" do
+     # テスト用のユーザーを作成する
+     user = create_user(1)
+     # テスト用のポストを作成する
+     posts = []
+     posts.unshift Post.create(content: "First Post!!", user: user)
+     posts.unshift Post.create(content: "Second Post!!", user: user) 
+     # user1でサインインする
+     sign_in(user)
+
+     # user1のプロフィールページにアクセスする
+     visit user_path(user)
+
+     # ポストのユーザー名がリンクになっていないことを検証する
+     posts.each_with_index do |post, i|
+       expect(find("#posts_list").all(".post-item")[i]).not_to have_selector("a.post-user-name")
+     end
+   end
  end

これもパスするはず。

# rspec spec/system/07_posts_spec.rb

Finished in 43.64 seconds (files took 6.54 seconds to load)
21 examples, 0 failures

パスしましたね。

ここまででポスト機能で定義したテストシナリオは全てパスできるアプリケーションを作ることができました!
最後に、今までのテストシナリオも含めてデグレの確認をしておきましょう!

# rspec

Finished in 1 minute 56.88 seconds (files took 6.09 seconds to load)
91 examples, 0 failures

2分ほど時間がかかりましたが、全てのテストをクリアできていました!!

まとめ

今日はここまでです!前回、前々回と3回に渡ってポスト機能をTDDでコーディングしてきましたがいかがだったでしょうか?
モデルの関連付け(has_many, belongs_to)や部分テンプレート(Partial Template)など新しく使ったものもありましたが、基本的な部分はハードルなくコーディングできるようになったのではないでしょうか?
実際にサービスをリリースするとなると、例えばアイコン登録とか、フォロー機能とか、いいね機能とか、、、作りたい機能がどんどんでてくるとは思いますが、すでに自分で調べながらコーディングをしていくことに対するハードルはなくなったんじゃないでしょうか?
ということでこのハンズオンのコーディング部分はこれで以上にしたいと思います。

最後はデプロイです!次とその次、2回に分けてアプリケーションをHerokuEKSにデプロイしてみようと思います。ここまでできれば、自分の好きなサービスを作って世に公開することができます。

ではまた次週!

後片付け

# exit
$ docker-compose down

本日のソースコード

Other Hands-on Links

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

任意のページにPV数(訪問者数)を計測、表示する【rails】

概要

webページを作成してリリースしたあと、大体何人見に来てくれているのか気になりますよね!以下のような「あなたは<51>人目の訪問者です」のような記述です。


PV数.JPG

アクセス回数などで調べる方法もありますが、同一人物が何度もアクセスしたら、延べ人数として重複して数えられてしまいます。これでは、より多くの人に見てもらいたいという意味では、正確な数字が測りにくくなります。また、他のQiita記事を見ると、impressionistとうgemで計測できるとあります。しかし、これはあくまで投稿詳細ページのPV数しか計測できず(投稿のidと紐づけられているため)任意のページのPV数が取得できません。そこで、ここでは、この二つの問題を"ある程度"解決できるIPアドレスを記録するという方法で訪問者数を計測していきます。(ログイン機能があれば、User数で知ることもできますね!)

IPアドレスとは

IPアドレスは機種ごと(パソコンやスマホ)に割り振られる固有の番号と知っている方も多いと思います。しかし、実際はやや異なるみたいです。世界で一つしかないIPアドレス(グローバルIP)はルーター(各家庭や各施設に一つあるインターネットとの出入口の役割を果たすもの)が持っており、その家庭内でのみ一意性が保たれるIPアドレス(プライベートIP)を各機種が持っているというのが一般的みたいです。後述のようにrailsでIPアドレスを取得する場合は、基本的にこのルーターのグローバルIPアドレスが取得されてしまうようです。したがって例えば、家庭内で同じWi-fiを使っている人同士は全て同一のIPアドレスとなります。何人いようが一人と計測されてしまうわけですね。もしくは、Wi-fiや機種を変えたり、インターネット環境を変えると、同一人物でも異なるIPアドレスが取得されてしまいます。このようにIPアドレスで訪問者数をカウントする場合は、"ある程度"の概算にならざるを得ないという事情があります。しかし、アクセス回数で計測する際の、製作者が気になって何度もwebページを見に来たり、利用者がPV数を誤魔化そうとその場で大量にアクセスしまくるといったPV数の不正確性に比べると、まだメリットは大きいのかなと個人的には感じています。

ipアドレス計測用のモデル(テーブル)作成

上記の事情を理解して頂けた方は、さっそく実装に入りましょう。まず、以下のようにipアドレスを計測するモデル(テーブル)を作成します。ターミナル(コマンドプロンプト)に入力してください。(プロダクトフォルダーの階層に移動することを忘れずに!)

$ rails generate model See ip:string

ここではSeeモデルという名前で作成します。(※データ型はstring型にしてください。ipアドレスはstring型で取得されるためです)するとmigrationファイルに

db/migration/******.rb
class CreateSees < ActiveRecord::Migration[*.*]
  def change
    create_table :sees do |t|
      t.string :ip

      t.timestamps
    end
  end
end

が作成されました。以下を実行してテーブルにカラムを適用させましょう。(ターミナル)

$ rails db:migrate

これにてIPアドレスを計測できるデータベースは完成しました。

Controllerの中身変更

次にControllerの中身に入ります。訪問者数を計測&表示したいviewに対応するアクションの中身を以下のように変えてください。ここでは例としてtweetsコントローラーのindexアクションを取り上げます。

app/views/tweets/index.html.erb
def index
  @see = See.find_by(ip: request.remote_ip) 
    if @see 
      @tweets = Tweet.all
    else 
      @tweets = Tweet.all
      See.create(ip: request.remote_ip)
    end
end

request.remote_ipで前述のようにアクセス者のIPアドレスが取得できます。何をしているかというと、まずseesテーブルで、アクセスした利用者のipアドレスと等しいipのレコードがあるか探し、あれば@seeに代入します。次に、もし、@seeが存在していれば(利用者が過去アクセスしたことがあれば)そのままviewページに受け渡す変数を書くだけです。もし、@seeが空だったら(利用者が初訪問の場合は)viewページに変数が受け渡されるとともに、seesテーブルのipカラムに、新たに利用者のIPアドレスが追加され保存されます。(検索機能などを付けている場合は、以下のように条件分岐が入れ子になります!)

app/views/tweets/index.html.erb
def index
  @see = See.find_by(ip: request.remote_ip)
    if @see 
      if params[:search]
        #部分検索
        @tweets = Tweet.where("content LIKE ? ",'%' + params[:search] + '%')
      else
        @tweets= Tweet.all
      end
    else 
      See.create(ip: request.remote_ip)
      if params[:search]
        #部分検索
        @tweets = Tweet.where("content LIKE ? ",'%' + params[:search] + '%')
      else
        @tweets= Tweet.all
      end
    end
end

viewページで訪問者数を表示する

これは簡単で以下の一文を任意の場所に追加するだけです。

あなたは<<%= See.count %>>人目の訪問者です 

これにより、過去訪問してきた、重複の無いIPアドレスの合計数が表示されます。(厳密には前述の理由と、今の新訪問者が未カウントなので、正確性を欠きますが。)今の"新"訪問者をカウントするなら

あなたは<<%= See.count + 1 %>>人目の訪問者です

このようになりますね!

最後に注意点

ローカル環境で開発する場合、このrequest.remote_ipでは、ローカルでデフォルトで用意されている"::1"というものがipアドレスとして取得されてしまいます。開発中にちゃんとできているか実験したくても、それは不可能です。言い換えると、開発環境においては、たとえ異なるパソコンでも全て同じデフォルトのIPアドレスになってしまうわけですね。ご注意ください。リリースしてからのお楽しみで!

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

[Rails]JS(jquery)が動かない 初歩的ミス

jquery使用の際、同じコード引用下のに別アプリで
なぜか動かない。
と言う原因の一例です。

確認した場所

■Gemfile→jqueryがインストールされているか
■application.jsにjqueryの記述があるか。
■js内にturbolinks:loadが入っているかどうか

原因

コメントアウトされているコードの並び順
*これ、すごく大事でした。
初歩の初歩でテキストに書いてあった気がする。。。

//= require_tree .が、最初はこの位置におりました。
image.png
移動!!最下部へ
image.png
こちらでjqueryが動かない問題は解決いたしました。

require_tree .とは?

参考資料によると以下の記載があります(ちなみにrequire_treeはcssにも記述あります)
そしてアセットパイプラインと言うそうです。
application内にあるcssやjsの読み込み順を司るものです。

require_treeディレクティブは、指定されたディレクトリ以下の
すべての JavaScriptファイルを再帰的にインクルードし、出力に含めます。
このパスは、マニフェストファイルからの相対パスとして指定する必要があります。
require_directoryディレクティブを使用すると、指定されたディレクトリの
直下にあるすべてのJavaScriptファイルのみをインクルードします。
この場合サブディレクトリを再帰的に探索しません。

日本語難しい(´・ω・)
少し簡単な説明

require の部分は ディレクティブ (何種類かあります)
require_directory
→与えられたディレクトリ以下のファイルを、
自身よりも前に挿入する。
順番はアルファベット順(さらに大文字→小文字)になる

require_tree
→require=directory と同じ動きをするが、再帰的に読み込む
読み込みはコードの 上から順番に 読み込まれます。

上記より考えると、require_treeより下に書かれていたjqueryが読み込まれなかったため動かなかった(と、解釈いたしました。)

[備考]
require_treeには引数として与えられたディレクトリ以下のファイルをアルファベット順に全て読み込むという意味があります。
現在require_treeの引数には.(ドット)が渡されています。
引数.(ドット)はカレントディレクトリを表します。
つまり、この記述によってapp/assets/javascriptsというディレクトリにあるファイルは全て読み込まれることになります。

参考ページ

Rails のアセットパイプライン(Asset Pipeline)について

RailsでCSSの読み込む順番を制御する方法

終わりに

こちらは、個人的解釈をもとに解決した方法を備忘録として書いております。
プログラミング初学者ゆえ、
誤記や不備、アドバイス等ございましたら御指摘いただけると幸いです。
最後まで読んでいただきありがとうございます。

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

【Rails】ルーティングをネストした時にshallowオプションを使うと便利だよ

ネストしたルーティングにshallowオプションを使うと便利だよ!と知ったので、自分なりに記述してみます?

ルーティングをネストする

  • このような親子関係のモデルがあるとする。
    • Boardモデルがhas_many :comments
    • Commentモデルがbelongs_to :board

以下の記述は、ルーティングをネストすることで、掲示板(Board)とコメント(Comment)の親子関係をルーティングで表しています。

config/routes.rb
resources :boards do
  resources :comments
end

次にターミナルでrails routesを実行し、利用可能なルーティングをすべて表示してみます。

  • rails routesで確認できる情報
    • ルーティング名 (あれば)
    • 使用されているHTTP動詞 (そのルーティングがすべてのHTTP動詞に応答するのでない場合)
    • マッチするURLパターン
    • そのルーティングで使うパラメータ
# rails routes
    board_comments GET    /boards/:board_id/comments(.:format)              comments#index
                   POST   /boards/:board_id/comments(.:format)              comments#create
 new_board_comment GET    /boards/:board_id/comments/new(.:format)          comments#new
edit_board_comment GET    /boards/:board_id/comments/:id/edit(.:format)     comments#edit
     board_comment GET    /boards/:board_id/comments/:id(.:format)          comments#show
                   PATCH  /boards/:board_id/comments/:id(.:format)          comments#update
                   PUT    /boards/:board_id/comments/:id(.:format)          comments#update
                   DELETE /boards/:board_id/comments/:id(.:format)          comments#destroy
            boards GET    /boards(.:format)                                 boards#index
                   POST   /boards(.:format)                                 boards#create
         new_board GET    /boards/new(.:format)                             boards#new
        edit_board GET    /boards/:id/edit(.:format)                        boards#edit
             board GET    /boards/:id(.:format)                             boards#show
                   PATCH  /boards/:id(.:format)                             boards#update
                   PUT    /boards/:id(.:format)                             boards#update
                   DELETE /boards/:id(.:format)                             boards#destroy

この状態だと、ある掲示板(board)のコメント(comments)の詳細ページをeditしたり、showしたりする時のURLは、
/boards/:board_id/comments/:id/edit
/boards/:board_id/comments/:id
といった風に指定する必要があり、冗長ですね。

shallowオプションの登場

そこで、ネストしたルーティングに、以下の様にshallow: trueを付けてあげます。

config/routes.rb
resources :boards do
  resources :comments, shallow: true
end
# rails routes
    board_comments GET    /boards/:board_id/comments(.:format)           comments#index
                   POST   /boards/:board_id/comments(.:format)           comments#create
 new_board_comment GET    /boards/:board_id/comments/new(.:format)       comments#new
      edit_comment GET    /comments/:id/edit(.:format)                   comments#edit
           comment GET    /comments/:id(.:format)                        comments#show
                   PATCH  /comments/:id(.:format)                        comments#update
                   PUT    /comments/:id(.:format)                        comments#update
                   DELETE /comments/:id(.:format)                        comments#destroy
            boards GET    /boards(.:format)                              boards#index
                   POST   /boards(.:format)                              boards#create
         new_board GET    /boards/new(.:format)                          boards#new
        edit_board GET    /boards/:id/edit(.:format)                     boards#edit
             board GET    /boards/:id(.:format)                          boards#show
                   PATCH  /boards/:id(.:format)                          boards#update
                   PUT    /boards/:id(.:format)                          boards#update
                   DELETE /boards/:id(.:format)                          boards#destroy

すると、index, new, createの3つのアクション(Commentのidを指定しないアクション)はBoardのidを必要としますが、それ以外のアクションは子のidを指定するだけで済むようになりました。
これは、

  • index、new、createは、Commentのidを指定しないため、Boardのidを指定しないと、どのBoardに対するものか参照できない。
  • show、edit、update、destroyは、Commentのidを指定するため、それだけで一意性を持てるから、Boardのidを指定する必要がない。

と考えると分かりやすいかなと思います!

参照先

Rails のルーティング(https://railsguides.jp/routing.html)

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