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

Rails初心者がActiveRecordの結合や複数条件に苦戦した話

ActiveRecordに苦戦しながらテーブル結合したりしてデータ取得した話です。

背景

Rails初心者がデータの更新や、複数テーブル、複数条件で指定する時があるかと思います。
今回はその時に調べたことを簡単にまとめていますので、興味がある方は是非。
なので、今回はRails初心者向けの内容ですね。

結論

まずはテーブルの結合、条件指定のやり方

TableA.joins(:table_b)
  .where(:table_b {id: 1})
  .where(id: 1)

joinsで内部結合しつつ、そのテーブルで条件を指定しています。
.where().where()でAND条件になっています。

そして、条件でorを使用するパターン
今回はvalueの範囲指定をorで繋げます。

TableA
  .where(foreign_id: 1)
  .where('value >= ?', 0)
  .or(TableA.where(foreign_id: 2 ).where(value: 0..100))
  .order(:sort)

foreign_idが1で、valueが0以上のデータと、
foregin_idが2でvalueが0以上100以下のデータを取得します。

以下は.whereで取得したRerationに対して更新しています。

relation.each do |data|
  # ここでdataの値を変更する

  data.save!
end

こうして変更したいデータ群の抽出と、それらのデータ更新を行いました。

何が大変だったか

ActiveRecordを使いながら知っていくのが大変でした。
.where()などを使うとrerationが返却されてきているので、.firstや.each()などを使用しないと例え1件しか取得していなくてもデータアクセスができません。それに、.load()を明示的に呼ばない限りはまだqueryを発行していないこともありますので、logger.debugの出力タイミングが意図しない順番になったりなど知っていないと難しい部分がありました。

基本だとは思いますが、クラスを調べれば大体なんとかなります。今回で言えばRelationクラスについて調べることで基本的に解決できました。
今後も複雑な条件が必要になってきたりすると思うのですが、その度に調べることになりそうです。
楽しみつつのRailsとの格闘はまだまだ続きそうです。

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

Vue.js チュートリアル for Rails エンジニア

はじめに

この記事は、普段 rails を使用して開発を行なっているエンジニアが、 Vue.js を触り始めようとする時に見たら役にたつかもしれないものです。

チュートリアルの内容

チュートリアルは、以下の2本立てで行うことで、 Vue.js に触れたことがない Rails エンジニアでも理解しやすい形にしています。すでに Vue.js に触れたことがある場合は、1を飛ばして、2から始めるのが良いかと思います。

  1. 簡単な Vue.js アプリケーションの開発
  2. Webpacker を使用した簡単な Rails + Vue.js アプリケーションの開発

使用するバージョン

  • ruby: 2.6.2
  • rails: 5.2.3
  • webpacker: 4.0.7
  • yarn: 1.16.0

1. 簡単な Vue.js アプリケーションの開発

を書こうと思ったら、すでにめちゃくちゃいいチュートリアル記事が既にあったので、そちらのリンクを掲載させていただきます。(タイトル詐欺)
Vue.js を vue-cli を使ってシンプルにはじめてみる

2. Webpacker を使用した簡単な Rails + Vue.js アプリケーションの開発

1 がだいたい終わって、 Vue.js のことをだいたい理解していることが前提です。
サンプルコードはこちら で公開しています。

環境構築

環境構築には、 homebrew , rbenv を使用します。インストールがまだな方は、各自インストールをお願いします。

rubyのバージョン設定

$ brew update && brew upgrade ruby-build
$ rbenv install 2.6.2

アプリケーションを作成するディレクトリに、 cd コマンドで遷移し、下記を実行してください。

$ rbenv local 2.6.2
$ ruby -v 
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-darwin18]

yarn のインストール

$ brew install yarn

インストールの確認(バージョンは下記のもの以上であれば基本問題ないと思います。)

$ yarn -v
1.16.0

Vue.js devtools のインストール

インストールがまだな場合は、下記のページを参考にインストールしておきましょう。
Vue.js Devtoolsの導入方法と機能まとめ。Vue.jsを用いた開発を効率化させよう!

プロジェクトの作成

下記コマンドを実行して、プロジェクトを作成します。

$ rails new rails-vue-app --webpack=vue --skip-turbolinks --skip-coffee

--skip-turbolinks --skip-coffee のオプションをつけて、今回は不要となるものを削ぎ落としています。 1

実行結果のログを眺めていると、 gem のインストールの後に、 rails webpacker:install から始まる webpacker 関連のインストールが行われていることがわかります。

rails  webpacker:install
RAILS_ENV=development environment is not defined in config/webpacker.yml, falling back to production environment

webpacker のインストールが完了すると、普段の rails プロジェクトでは見られない、 app/javascript というディレクトリが、自動で生成されているかと思います。

$ cd rails-vue-app/app/javascript
$ ls
  app.vue packs

※ 今回の実装では、新規に Rails + Vue.js のプロジェクトを作ることを想定していますが、もちろん既存の Rails プロジェクトに途中から Vue.js を導入することもできます。2

それでは、これから rails アプリケーションの中身の実装を進めていこうと思います。

アプリケーションの実装

一番シンプルな実装

まずは、 rails 上で vue.js を動かす一番シンプルな実装をしていきます。
cd コマンドなどで先ほど作成したディレクトリに移動し、まずはコントローラーを作成します。
※ 今後 [] はコマンドを実行するディレクトリを表します。

[rails-vue-app] $ rails g controller HelloVue index --no-helper --no-assets
      create  app/controllers/hello_vue_controller.rb
       route  get 'hello_vue/index'
      invoke  erb
      create    app/views/hello_vue
      create    app/views/hello_vue/index.html.erb
      invoke  test_unit
      create    test/controllers/hello_vue_controller_test.rb

※ 今回は、ヘルパーやアセットファイルなど不要なものを生成しないオプションを指定しています。 3

一旦サーバーを起動して、画面が表示するかみて見ましょう。

[rails-vue-app] $ rails s

http://localhost:3000/hello_vue/index にアクセスして、下記のページが表示されると ok です。

javascript_pack_tag の設定

javascript_pack_tagapp/javascript/packs 配下にあるファイルを読み込むことができます。 (この辺は webpackerの仕様で決まっています)
今回は、読み込む対象として hello_vue.js を指定してみます。 hello_vue.js は webpacker のインストール時に自動生成されているファイルです。

app/views/hello_vue/index.html.erb
  <h1>HelloVue#index</h1>
  <p>Find me in app/views/hello_vue/index.html.erb</p>
+ <%= javascript_pack_tag 'hello_vue.js' %>

hello_vue.js の中身は、下記のような感じになっており、同じく初期作成された app.vue ファイルを描画するようになっています。

app/javascript/packs/hello_vue.js
import Vue from 'vue'
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

  console.log(app)
})
app/javascript/app.vue
  <div id="app">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      message: "Hello Vue!"
    }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

これらのファイルがどう言う役割をしているのかが曖昧な場合は、もう一度 簡単な Vue.js アプリケーションの開発 の項を見てみましょう。

webpacker を使用した webpack のビルド

下記のコマンドを実行することで、 app/javascript 配下の js ファイルをビルドすることができます。

[rails-vue-app] $ bin/webpack

このコマンドは、内部的には下記のコマンドを実行しているのにほぼ等しいらしいです。(知らなかった)

[rails-vue-app] ./node_modules/.bin/webpack --config config/webpack/development.js

Webpacker使うなら最低限これだけは知っておいてほしいこと から抜粋させてもらいました。

再度 http://localhost:3000/hello_vue/index にアクセスしてみると、下記のような画面になっているかと思います。

image.png

環境構築で、 Vue.js devtools のインストールが完了している場合、 chrome の拡張機能の部分に、 V のマークが色付きで出てきているはずです。
image.png

この時点で、一番の基本となる rails + Vue.js のアプリケーション実装が完了しました。:tada:

webpack の自動ビルド

先ほどは、 bin/webpack のビルドを行いましたが、これでは js ファイルを変更するたびに再度コマンドを叩いてビルドをする必要があります。
流石にそれは面倒なので、開発中は bin/webpack の代わりに下記コマンドを実行して、ファイルを保存するたびに自動ビルドが走るようにしておくといいでしょう。

[rails-vue-app] $ bin/webpack-dev-server

Vue.js devtools

せっかくなので Vue.js devtools の使い方をここで確認しておきます。
使い方は簡単で、 chrome の developer ツールを開いて、 Vue のタブを選択するだけです。

Vue のタブを選択し、コンポーネントを選択してみると、内部の data などを確認することができます。

実践的な実装

先ほど実装はただ単に、作成した vue ファイルの描画を行っただけで、 controller などからのデータの受け渡しを行なっていませんでした。そこで、ここからはその点を深掘っていきます。

webpacker を使用した、 rails + vue のアプリケーションを作成する際、データの渡し方には色々あるらしいですが、今回は2通りのデータの受け渡し方でサンプルアプリケーションを作っていきます(個人的にはこの2つの渡し方は、データフローがわかりやすい)。

  • HTML のデータ属性に値を設定して渡す方法
  • API を使用して渡す方法

前者の方法については、下記の記事を参考にさせていただきました。
メンテ不能になったフロントエンド環境を立て直す話

HTML のデータ属性に値を設定して渡す方法

まずはページを表示するためのコントローラーを、 HomeController という名前で作成します。

$ cd rails-vue-app
[rails-vue-app] $ rails g controller Home index --no-helper --no-assets
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
       exist    app/views/home
      create    app/views/home/index.html.erb
      invoke  test_unit
      create    test/controllers/home_controller_test.rb

次にコントローラー内部の実装を進めていきます。 index メソッドのインスタンス変数として、 title description そして Hash 形式の contents を用意します。

app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
+   @title = 'Home#index'
+   @description = 'トップページ'
+   @contents = get_contents
  end
+
+ private
+
+ def get_contents
+   {
+     outer_links: [
+       {
+         name: 'Qiitaページ',
+         text: 'Qiita',
+         url: 'https://qiita.com/t0yohei/items/d516fefaaad69b4022ec'
+       },
+       {
+         name: 'ソースコード',
+         text: 'GitHub',
+         url: 'https://github.com/t0yohei/rails-vue-app'
+       }
+     ],
+   }
+ end
end

続いて view の実装です。ここでポイントとなるのが、 rails の content_tag ヘルパーを利用して、 div タグの data 属性に vue 側に受け渡したいデータ設定している点です。 vue に受け渡すデータは json 形式に変換しておきます。

app/views/home/index.html.erb
- <h1>Home#index</h1>
- <p>Find me in app/views/home/index.html.erb</p>
+ <%= javascript_pack_tag 'home/index.js' %>
+ <%= content_tag :div,
+   id: "homeIndex",
+   data: {
+     title: @title,
+     description: @description,
+     contents: @contents
+   }.to_json do %>
+ <% end %>

生成される html は下記画像のようになります。
image.png

data 属性に設定した情報は、 developer tool などを使うことで閲覧することができるので、 API 同様にユーザーのプライペート情報など、秘匿情報は公開しないようにして注意してください。

表示するコンポーネントの実装

先ほど view で設定したデータを、 js 側から読み取ってみましょう。 app/javascript/packs/home/index.js を下記の通り実装します。

app/javascript/packs/home/index.js
import Vue from "vue";

document.addEventListener("DOMContentLoaded", () => {
  const node = document.getElementById("homeIndex");
  const props = JSON.parse(node.getAttribute("data"));
  console.log(props);
});

http://localhost:3000/home/index にアクセスして、 developer tool の console を開けてみると、
image.png

このように、 rails の view ファイルで設定したデータが、JS の Object 形式で取得できていることがわかります。
この Object データを使用して、実装を進めていきましょう。

home/index.js では、 home/Index というコンポーネントを render することを想定して実装を進めていきます。 render 関数の第二引数に、先ほど取得した Object のデータを設定します。

app/javascript/packs/home/index.js
 import Vue from "vue";
+import Index from "../../components/home/Index.vue";

 document.addEventListener("DOMContentLoaded", () => {
   const node = document.getElementById("homeIndex");
   const props = JSON.parse(node.getAttribute("data"));
+  const app = new Vue({
+    render: h => h(Index, { props })
+  }).$mount();
+  document.body.appendChild(app.$el);
-  console.log(props);
 });

render 対象の、 home/Index コンポーネントでは、受け取る props を定義しておきます。これで home/index.js からデータを受け取って、コンポーネント内で参照することができます。

app/javascript/components/home/Index.vue
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
    <table class="contents-table">
      <tr>
        <th>名前</th>
        <th>リンク</th>
      </tr>
      <tr v-for="outer_link in contents.outer_links" v-bind:key="outer_link.name">
        <td>{{ outer_link.name }}</td>
        <td>
          <a v-bind:href="outer_link.url">{{ outer_link.text }}</a>
        </td>
      </tr>
    </table>
  </div>
</template>

<style scoped>
.contents-table {
  border: 1px solid gray;
  margin: 10px;
}
.contents-table th,
.contents-table td {
  border: 1px solid gray;
}
</style>

<script>
export default {
  props: {
    title: {
      type: String,
      default: () => ""
    },
    description: {
      type: String,
      default: () => ""
    },
    contents: {
      type: Object,
      default: () => {}
    }
  }
};
</script>

ページの表示

app/javascript/packs 配下に追加した js ファイルを読み込ませるためには、 webpack-dev-server の再起動が必要です。 webpack-dev-server を実行中の場合は一度止めて、再度下記コマンドを実行しましょう。

[rails-vue-app] $ bin/webpack-dev-server

http://localhost:3000/home/index にアクセスした時に、下記のようなページが表示されていると成功です。

実装のリファクタリング

とりあえず表示させることを優先で、 Index.vue に全てを書いていたので、簡単にコンポーネントの分割をしていきます。ざっとこんな感じのイメージで分割していきます。
image.png

HeaderView.vue の作成

app/javascript/components/HeaderView.vue
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>
</template>
<style></style>
<script>
export default {
  props: {
    title: {
      type: String,
      default: () => ""
    },
    description: {
      type: String,
      default: () => ""
    }
  }
};
</script>

Contents.vue の作成

app/javascript/components/home/Contents.vue
<template>
  <div>
    <table class="contents-table">
      <tr>
        <th>名前</th>
        <th>リンク</th>
      </tr>
      <tr v-for="outer_link in contents.outer_links" v-bind:key="outer_link.name">
        <td>{{ outer_link.name }}</td>
        <td>
          <a v-bind:href="outer_link.url">{{ outer_link.text }}</a>
        </td>
      </tr>
    </table>
  </div>
</template>
<style scoped></style>
<script>
export default {
  props: {
    contents: {
      type: Object,
      default: () => {}
    }
  }
};
</script>

Index.vue の修正

app/javascript/components/home/Index.vue
 <template>
   <div>
-    <h1>{{ title }}</h1>
-    <p>{{ description }}</p>
-    <table class="contents-table">
-      <tr>
-        <th>名前</th>
-        <th>リンク</th>
-      </tr>
-      <tr v-for="outer_link in contents.outer_links" v-bind:key="outer_link.name">
-        <td>{{ outer_link.name }}</td>
-        <td>
-          <a v-bind:href="outer_link.url">{{ outer_link.text }}</a>
-        </td>
-      </tr>
-    </table>
+    <header-view v-bind:title="title" v-bind:description="description"></header-view>
+    <contents v-bind:contents="contents"></contents>
   </div>
 </template>

-<style scoped>
-.contents-table {
-  border: 1px solid gray;
-  margin: 10px;
-}
-.contents-table th,
-.contents-table td {
-  border: 1px solid gray;
-}
-</style>
+<style scoped></style>

 <script>
+import HeaderView from "../HeaderView.vue";
+import Contents from "./Contents.vue";
+
 export default {
+  components: {
+    "header-view": HeaderView,
+    contents: Contents
+  },
  props: {
    title: {
      type: String,
      default: () => ""
    },
    description: {
      type: String,
      default: () => ""
    },
    contents: {
      type: Object,
      default: () => {}
    }
  }
};
</script>

だいぶスッキリしましたね。リファクタリングは一旦こんな感じで終わりましょうか。
念の為、再度 http://localhost:3000/home/index にアクセスして、画面がちゃんと表示されることを確認しておきましょう。

API を使用して渡す方法での実装

次に、API を使用して渡す方法での実装を進めていきましょう。
今回は特に理由もないんですが、整数リテラルの分類表を作成してみます。
手順としては、

  • APIを叩いて取得したデータを受け取るコンポーネントの実装
  • APIエンドポイントの実装
  • コンポーネントから API を叩いてデータを取得
  • 取得したデータをコンポーネント内で描画

という順番で進めていきます。

APIを叩いて取得したデータを受け取るコンポーネントの実装

まずはページを表示するためのコントローラーを作成します。

$ cd rails-vue-app
[rails-vue-app] $ rails g controller IntegerLiteralDescriptions index --no-helper --no-assets
      create  app/controllers/integer_literal_descriptions_controller.rb
       route  get 'integer_literal_descriptions/index'
      invoke  erb
      create    app/views/integer_literal_descriptions
      create    app/views/integer_literal_descriptions/index.html.erb
      invoke  test_unit
      create    test/controllers/integer_literal_descriptions_controller_test.rb

コントローラーの実装

今回は何もしないです。

View の実装

javascript_pack_tag を設定します。

app/views/integer_literal_descriptions/index.html.erb
- <h1>IntegerLiteralDescriptions#index</h1>
- <p>Find me in app/views/integer_literal_descriptions/index.html.erb</p>
+ <%= javascript_pack_tag 'integerLiteralDescriptions/index.js' %>

表示するコンポーネントの実装

integerLiteralDescriptions/index.js の実装

app/javascript/packs/integerLiteralDescriptions/index.js
import Vue from "vue";
import Index from "../../components/integerLiteralDescriptions/Index.vue";

document.addEventListener("DOMContentLoaded", () => {
  const app = new Vue({
    render: h => h(Index)
  }).$mount();
  document.body.appendChild(app.$el);
});

コンポーネントの実装

app/javascript/components/integerLiteralDescriptions/Index.vue
<template>
  <div>
    <header-view v-bind:title="title" v-bind:description="description"></header-view>
    <contents v-bind:contents="contents"></contents>
  </div>
</template>

<script>
import HeaderView from "../HeaderView.vue";
import Contents from "./Contents.vue";

export default {
  components: {
    "header-view": HeaderView,
    contents: Contents
  },
  data: function() {
    return {
      title: "title",
      description: "description",
      contents: []
    };
  }
};
</script>

<style scoped>
</style>
app/javascript/components/integerLiteralDescriptions/Contents.vue
<template>
  <div>
    <table class="contents">
      <tr>
        <th>名前</th>
        <th>英語訳</th>
        <th>表記例</th>
        <th>用途</th>
      </tr>
      <tr v-for="content in contents" v-bind:key="content.name">
        <td>{{ content.name }}</td>
        <td>{{ content.english }}</td>
        <td>{{ content.sample }}</td>
        <td>{{ content.usage }}</td>
      </tr>
    </table>
  </div>
</template>
<style scoped>
.contents {
  border: 1px solid gray;
}
.contents th,
.contents td {
  border: 1px solid gray;
}
</style>
<script>
export default {
  props: {
    contents: Array
  }
};
</script>

再び bin/webpack-dev-server を実行し直し、 http://localhost:3000/integer_literal_descriptions/index にアクセスすると、下記画像のようなページが表示されるかと思います。

これで API を叩いて取得したデータを受け取り、表示するためのコンポーネントが完成しました。

次は、先ほど作成したコンポーネントに、データを渡す処理を実装していこうと思います。

APIエンドポイントの実装

[rails-vue-app] rails g controller api/v1/integer_literal_descriptions index --no-helper --no-assets --no-view-specs
      create  app/controllers/api/v1/integer_literal_descriptions_controller.rb
       route  namespace :api do
  namespace :v1 do
    get 'integer_literal_descriptions/index'
  end
end
      invoke  erb
      create    app/views/api/v1/integer_literal_descriptions
      create    app/views/api/v1/integer_literal_descriptions/index.html.erb
      invoke  test_unit
      create    test/controllers/api/v1/integer_literal_descriptions_controller_test.rb

コントローラーの実装

app/controllers/api/v1/integer_literal_descriptions_controller.rb
 class Api::V1::IntegerLiteralDescriptionsController < ApplicationController
   def index
+    title = 'IntegerLiteralDescriptions#index'
+    description = '整数リテラルの分類表'
+    contents = get_integer_literals
+    result_values = {
+      title: title,
+      description: description,
+      contents: contents
+    }
+    render json: result_values
+    # https://jsprimer.net/basic/data-type/#integer-literal
   end
-end
+
+  private
+
+  def get_integer_literals
+    [
+      {
+        name: '10進数',
+        english: 'decimal',
+        sample: '42',
+        usage: '数値'
+      },
+      {
+        name: '2進数',
+        english: 'binary digits',
+        sample: '0b0001',
+        usage: 'ビット演算など'
+      },
+      {
+        name: '8進数',
+        english: 'octal',
+        sample: '0o777',
+        usage: 'ファイルのパーミッションなど'
+      },
+      {
+        name: '16進数',
+        english: 'hexadecimal, hex',
+        sample: '0xEEFF',
+        usage: '文字のコードポイント、RGB値など'
+      }
+    ]
+  end
+end

アクセスの確認

この状態で、 http://localhost:3000/api/v1/integer_literal_descriptions/index にアクセスしてみると、下記のような画面が表示されるはずです。
 
image.png

一旦これで、 API のエンドポイントが完成しました。

コンポーネントから API を叩いてデータを取得

コンポーネントと API の Ajax 通信は axios というライブラリを使用して行います。4
まずは axios をインストールします。

[rails-vue-app] yarn add --dev axios

install に成功していると、 package.json axios の項目が追記されているはずです。

package.json
   "devDependencies": {
+    "axios": "^0.19.0",
     "webpack-dev-server": "^3.7.2"
   }

次に integerLiteralDescriptions/Index.vue を書き換えて、 axios でのデータ取得を実装します。

app/javascript/components/integerLiteralDescriptions/Index.vue
 import Contents from "./Contents.vue";
+import Axios from "axios";

 export default {
   components: {
     "header-view": HeaderView,
     contents: Contents
   },
   data: function() {
     return {
       title: "title",
       description: "description",
       contents: []
     };
+  },
+
+  created: function() {
+    this.updateContents();
+  },
+
+  methods: {
+    updateContents() {
+      Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
+        response => {
+          const responseData = response.data;
+          console.log(responseData);
+        }
+      );
+    }
   }

created で vue コンポーネントが作成されたタイミングで axios によるデータ取得を走らせるようにしています。
console.log で取得したデータを表示するようにしているので、 http://localhost:3000/integer_literal_descriptions/index を見てみましょう。
↓のようなログが出て、データ取得ができているはずです。
image.png

Tips: JS のデバッグ

ご存知な方も多いと思いますが、 JS では debugger を仕込むことで、デバッグ実行が可能になります。
debugger ステートメント | MDN

具体的には下記のように仕込むことができます。

app/javascript/components/integerLiteralDescriptions/Index.vue
   Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
     response => {
       const responseData = response.data;
-      console.log(responseData);
+      debugger;
     }
   );

開発者ツールを開きながら、再度先ほどの http://localhost:3000/integer_literal_descriptions/index にアクセスしてみると、
image.png

debugger を仕込んだ部分で処理が止まり、 Console からその時点の各種データを覗くことができます。(↑画像の場合、画像下部で responseData の値を確認しています。)

取得したデータをコンポーネント内で描画

先ほど axios で取得したデータを、画面に反映させます。
今回の場合、下記の部分を書き換えるだけです。

app/javascript/components/integerLiteralDescriptions/Index.vue
   Axios.get("/api/v1/integer_literal_descriptions/index.json").then(
     response => {
       const responseData = response.data;
-      console.log(responseData);
+      this.title = responseData.title;
+      this.description = responseData.description;
+      this.contents = responseData.contents;
     }
   );

http://localhost:3000/integer_literal_descriptions/index にアクセスしてみると、整数リテラル分類表が出てくるはず!

整数リテラル分類表

JS では 0 で始まる数値の直後に b, o, x をつけると、それぞれ2進数、8進数、16進数が表現できるみたいですね。 b, o, x は 英語訳を見てみると、それぞれ binary digits, octal, hex となっており、なるほどなーとなるんじゃないでしょうか。今回整数リテラル分類表をサンプルに組み込んだ理由は特にないです。気まぐれです。

最後にちょろちょろ

せっかくなので、 http://localhost:3000/home/index から http://localhost:3000/integer_literal_descriptions/index に飛べるように、リンクボタンを追加しておきましょう。

app/controllers/home_controller.rb
           url: 'https://github.com/t0yohei/rails-vue-app'
         }
       ],
+      inner_links: [{
+        label: '整数リテラル分類表',
+        url: url_for(action: 'index', controller: 'integer_literal_descriptions')
+      }]
     }
app/javascript/components/home/Contents.vue
         </td>
       </tr>
     </table>
+    <div v-for="inner_link in contents.inner_links" v-bind:key="inner_link.label">
+      <button v-on:click="changeLocation(inner_link.url)" class="btn-push">{{ inner_link.label }}</button>
+    </div>
   </div>
 </template>
 <style scoped>
@@ -23,6 +26,23 @@
 .contents-table td {
   border: 1px solid gray;
 }
+.btn-push {
+  margin: 10px;
+  max-width: 180px;
+  text-align: left;
+  background-color: rgb(24, 174, 238);
+  font-size: 14px;
+  color: #fff;
+  text-decoration: none;
+  font-weight: bold;
+  padding: 10px 24px;
+  border-radius: 4px;
+  border-bottom: 4px solid rgb(24, 174, 238);
+}
+.btn-push:active {
+  transform: translateY(4px);
+  border-bottom: none;
+}
 </style>
 <script>
 export default {
@@ -31,6 +51,11 @@ export default {
       type: Object,
       default: () => {}
     }
+  },
+  methods: {
+    changeLocation(url) {
+      window.location.href = url;
+    }
   }
 };
 </script>

http://localhost:3000/home/index を開いて、こんな感じのボタンができていたら成功です。

最後に

今回のチュートリアルはここで終了です。 会社の人が Vue.js を触り始める時に使ってもらえたらなーと思いこのチュートリアルの作成を計画したのですが、せっかくなので Qiita に投稿してみることにしました。どこかのエンジニアの役に立つと幸いです。


  1. 今回の実装では CoffeeScriptturbolinks のセットアップを省いています。既存の rails プロジェクトで Vue.js を使用する場合は、 turbolinks と戦う必要がありそうです。 

  2. もし既存の rails プロジェクトに途中から導入する場合は、右記をご参考ください。rails/webpacker#installation 

  3. オプションの詳細は、 こちらrails g controller -h コマンドで確認できます。 

  4. axios を利用した API の使用 

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

HTMLでテーブルのヘッダー(上部と左部)を固定してスクロールさせる実装

はじめに

テーブルのヘッダー(上部と左部)を固定してスクロールさせる実装でプラグインなどを比較したため、備忘録を含めまとめたいと思います。

position:stickの実装方法

まず、大前提として本来であればposition:stickyを使用することが望ましいかと思いました。
こちらの方の記事が大変わかりやすいです。
https://qiita.com/s0tter/items/14fb4ec2600828a21a22
CSSのみで完結するため上記手法が望ましいかと考えておりますが、2019年7月7日時点でIEに対応しておりません。IEのユーザーもアプリを活用されることが予想されるため、採用を見送りさせていただきました。
https://caniuse.com/#search=position%3A%20sticky

position:sticky対応状況

スクリーンショット 2019-07-07 17.43.02.png

可能であれば、position:stickyを使用したいのですが、今回はIEの関係で採用が難しいため、以下のプラグイン等を比較いたしました。

プラグインを比較してみました。

当方Macしかパソコンを持ち合わせていないため、virtualBoxでIEを立ち上げて
動作状況を確認してみました。

・DataTable

Github:https://github.com/DataTables/DataTables
https://datatables.net/extensions/fixedcolumns/examples/initialisation/two_columns.html
最終更新:2018年6月23日

IEでの動作状況

ezgif.com-video-to-gif.gif

・fixedTblHdrLftCol

Github:https://github.com/nkmrshn/fixedTblHdrLftCol
http://nkmrshn.com/fixedTblHdrLftCol/samples/sample_3_sync.html
最終更新:2014年6月13日

IEでの動作状況

ezgif.com-video-to-gif.gif

・FixwdMidashi

http://hp.vector.co.jp/authors/VA056612/fixed_midashi/manual/index.html
最終更新:2018年12月3日

IEでの動作状況

ezgif.com-video-to-gif.gif

・Grid

(まだまとめている最中です。)

IEで最も綺麗に動作しているのが、FixwdMidashiだったように思えます。
他に良い方法などございましたら、一声かけていただけますと幸いです。

少しずつ追加させていただきます。
ご参考になれば幸いです。

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

【MySQL】Mysql2::Error: Incorrect string value 【エラー】

 開発環境では問題なく行えていたseedファイルの読み込みですが、本番環境ではseedファイルを読み込む際に、MySQLエラーが発生しました。その対応について記述します。

1. エラー発生状況とエラー内容

ターミナル(本番環境)
# seedファイルの読み込みコマンド
$ rails db:seed RAILS_ENV=production
# 発生したエラー
 ActiveRecord::StatementInvalid: Mysql2::Error: Incorrect string value: '\xE5\x8C\x97\xE6\xB5\xB7...' for column 'name' at row 1: INSERT INTO 'areas ('name') VALUES ('北海道')

2. 開発環境と本番環境の文字コードを確認

 まずは、ローカル環境を確認する

ローカル環境
-- mysqlにログイン後、データベース一覧を出力
mysql> SHOW databases;
-- 該当のdatabaseを確認後、databaseを選択
mysql> use applicable_database --[該当のデータベース名]
-- 文字コードを確認
mysql> show variables like '%char%';
+--------------------------+------------------------------+
| Variable_name            | Value                        |
+--------------------------+------------------------------+
| character_set_client     | utf8                         |
| character_set_connection | utf8                         |
| character_set_database   | utf8                         |
| character_set_filesystem | binary                       |
| character_set_results    | utf8                         |
| character_set_server     | latin1                       |
| character_set_system     | utf8                         |
| character_sets_dir       | /usr/share/mysql56/charsets/ |
+--------------------------+------------------------------+

 同様に、本番環境を確認すると

本番環境
mysql> show variables like '%char%';
+--------------------------+------------------------------+
| Variable_name            | Value                        |
+--------------------------+------------------------------+
| character_set_client     | utf8                         |
| character_set_connection | utf8                         |
| character_set_database   | latin1                       |
| character_set_filesystem | binary                       |
| character_set_results    | utf8                         |
| character_set_server     | latin1                       |
| character_set_system     | utf8                         |
| character_sets_dir       | /usr/share/mysql56/charsets/ |
+--------------------------+------------------------------+

 上記を比べると、character_set_databaseValue
 ローカル環境では、utf8
 本番環境では、latin1
 となっています。

 latin-1では日本語対応していないため、MySQLエラーが出ていたみたいですね。
 ためしに、本番環境のdatabaseを削除して、utf-8を指定して作り直してみました。

本番環境
-- mysqlにログイン後、データベース一覧を出力します
mysql> SHOW databases;
-- 該当のdatabaseを確認後、削除コマンドによりデータベースを削除します
mysql> drop database applicable_database --[該当のデータベース名]
-- 同様の名前でdatabaseを作成、ただしオプションで文字コードを指定する
mysql> create database book_reviews_production default character set utf8;
-- databaseの文字コードを確認する
mysql> show variables like '%char%';
+--------------------------+------------------------------+
| Variable_name            | Value                        |
+--------------------------+------------------------------+
| character_set_client     | utf8                         |
| character_set_connection | utf8                         |
| character_set_database   | utf8                         |
| character_set_filesystem | binary                       |
| character_set_results    | utf8                         |
| character_set_server     | latin1                       |
| character_set_system     | utf8                         |
| character_sets_dir       | /usr/share/mysql56/charsets/ |
+--------------------------+------------------------------+

 文字コードの変更できました。
 この後のmigrationとseedの読み込みコマンドは問題なく行えました。

3. 原因

 database.ymlを確認してみると、charsetの指定が抜けていました
 追記することで、Railsコマンドでdatabaseを作成した際にも、文字コードが変更されました。

database.yml
default: &default
  adapter: mysql2
  # ↓これ
  charset: utf8
  encoding: utf8
  pool: 5
  username: root
  password:
  socket: /tmp/mysql.sock

4. あとがき

 調べてみると、databaseをdropすることなく文字コードの変更も行えるみたいです。
 重要なレコードがある場合は、そちらをご活用ください。

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

Don't know how to build task 'YOUR CREATE TASK'

ファイル拡張子を rake にしましょう!

rbにしてた:upside_down:

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

Herokuの本番環境でRansack検索した時にエラー「PG::SyntaxError: ERROR: syntax error at or near "DISTINCT"」

問題:Herokuの本番環境でRansack検索した時にエラー「PG::SyntaxError: ERROR: syntax error at or near "DISTINCT"」

ローカルでは問題ないのに本番環境で検索すると
「We're sorry, but something went wrong.」のエラーが出てしまった

ログを見てみるとこんな感じ

ActionView::Template::Error (PG::SyntaxError: ERROR:  syntax error at or near "DISTINCT"
2019-07-07T08:13:50.904602+00:00 app[web.1]: LINE 1: SELECT DISTINCT DISTINCT ON (latitude)* FROM "events" WHERE ...
2019-07-07T08:13:50.904604+00:00 app[web.1]: ^
2019-07-07T08:13:50.904606+00:00 app[web.1]: : SELECT DISTINCT DISTINCT ON (latitude)* FROM "events" WHERE "events"."prefecture_id" = $1 AND "events"."play_type" IN (1) ORDER BY latitude,id asc):

distinct付近に何かしら問題があるぽい
controllerはこんな感じ

class PrefecturesController < ApplicationController
  def show
    @prefecture = Prefecture.find(params[:id])
    @events = @prefecture.events
    @q = Event.ransack
    if Rails.env == 'production'
      @venues = @events.select("DISTINCT ON (latitude)*").order("latitude,id asc")
    else
      @venues = @events.group(:latitude).order("id asc")
    end
    gon.venues = @venues
    duplicate = @events.group(:latitude).having('count(*)>=2').pluck(:latitude)
    gon.events = Event.where(latitude: duplicate).offset(1)
  end

  def search
    @prefecture = Prefecture.find(params[:id])
    @q = @prefecture.events.ransack(search_params)
    @events = @q.result(distinct: true)
    if Rails.env == 'production'
      @venues = @events.select("DISTINCT ON (latitude)*").order("latitude,id asc")
    else
      @venues = @events.group(:latitude).order("id asc")
    end
    gon.venues = @venues
    duplicate = @events.group(:latitude).having('count(*)>=2').pluck(:latitude)
    gon.events = @events.where(latitude: duplicate).offset(1)
  end

  private

  def search_params
    params.require(:q).permit(:title_cont,{:dayw_in => []},{:level_in => []},{:play_type_in => []},:status_eq)
  end
end

showとsearchアクションはご覧の通り殆ど一緒です
違うのは検索クエリ?の@q変数が関わっているところだけ
その中でdistinct使ってるのはこの一文のみ

@events = @q.result(distinct: true)

もしかしたらこいつのせいかもと思って消してみたら...
動いたー

(distinct: true)の意味を調べてみる

そもそも(distinct: true)をオプションでつけると何が変わるのか確認
重複したレコードを除外してくれらしい
SQLはこんな感じ

SELECT DISTINCT `events`.* FROM `events` WHERE `events`.`prefecture_id` = 22 AND `events`.`play_type` IN (1)
SELECT `events`.* FROM `events` WHERE `events`.`prefecture_id` = 22 AND `events`.`play_type` IN (1)

仮説

@events = @q.result(distinct: true)

この一文で全てのカラムが重複しないレコードを取り出しているのに

@venues = @events.select("DISTINCT ON (latitude)*").order("latitude,id asc")

ここでまたlatitudeが一意のデータを抽出しようとしてるからエラーになったんだと思う

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

Herokuへデプロイした時に「Could not detect rake tasks」エラーが出る

事象

Railsで作ったアプリをHerokuにデプロイしたら下記のエラーが、ターミナルのログに表示。

remote:  !
remote:  !     Could not detect rake tasks
remote:  !     ensure you can run `$ bundle exec rake -P` against your app
remote:  !     and using the production group of your Gemfile.
remote:  !     Activating bundler (2.0.1) failed:
remote:  !     Could not find 'bundler' (2.0.1) required by your /tmp/build_242d76def6eeeeae57603bbca28c0d6b/Gemfile.lock.
remote:  !     To update to the latest version installed on your system, run `bundle update --bundler`.
remote:  !     To install the missing version, run `gem install bundler:2.0.1`
remote:  !     Checked in 'GEM_PATH=/tmp/build_242d76def6eeeeae57603bbca28c0d6b/vendor/bundle/ruby/2.6.0', execute `gem env` for more information
remote:  !     
remote:  !     To install the version of bundler this project requires, run `gem install bundler -v '2.0.1'`
remote:  !
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/limitless-eyrie-60171.git'

原因

bundlerのバージョンか、Gemfileっぽい

参考記事
https://qiita.com/satouwork0316/items/a738392bcab03bac657a

対処方法

Gemfileの記述?
試しに、以前デプロイに成功したアプリのGemfileをコピペして上書き。

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.2'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.3'
# 20190622 bcrypt追加※パスワードハッシュ化
gem 'bcrypt', '3.1.12'
# 20190617 bootstrap追加
gem 'bootstrap-sass', '3.3.7'
# Use Puma as the app server
gem 'puma', '~> 3.11'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'mini_racer', platforms: :ruby

# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

# 20190626 Jquery gem追加
gem 'jquery-rails'
gem 'jquery-ui-rails'

group :development, :test do
    # Use sqlite3 as the database for Active Record
    gem 'sqlite3'
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :test do
  # Adds support for Capybara system testing and selenium driver
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  # Easy installation and use of chromedriver to run system tests with Chrome
  gem 'chromedriver-helper'
    # 20190615追記
    gem 'rails-controller-testing', '1.0.2'
  gem 'minitest',                 '5.11.3'
  gem 'minitest-reporters',       '1.1.14'
  gem 'guard',                    '2.13.0'
  gem 'guard-minitest',           '2.4.4'
end

# 20190615追記
group :production do
  gem 'pg', '0.20.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

下記のコマンドをターミナルで叩いて、再度デプロイ

gem install bundler -v 2.0.2
bundle update --bundler

デプロイ出来た!!(解決)

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

Herokuへデプロイした時に「The page you were looking for doesn't exist. 」エラーが出る

事象

Railsでアプリを作って、動作確認も問題なし!
「さぁHerokuにデプロイ!」と思い、「git push heroku master」を実施。

デプロイされたアプリをherokuのURLを開くと下記のエラーが出た・・・

スクリーンショット 2019-07-07 16.37.45.png

The page you were looking for doesn't exist.
You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

ターミナル上でもデプロイ時にエラーが出ておらず原因が不明。

原因

ググってみると、どうやらroutes.rbにroot(URLのトップ)にアクセスした際に表示するページが設定されていないのが問題っぽい。

参考記事
http://mozuzaru.hatenablog.com/entry/2018/03/10/204643

解決方法

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

    root  'users#index'  #この1行を追加

    #ユーザー一覧を表示
    get '/users', to: 'users#index'
    #新規投稿(登録画面)に遷移
    get '/users/new', to: 'users#new'
    #投稿されたデータを受け取る
    post '/users', to: 'users#create'

    #投稿されてデータを編集する画面に行く
    get '/users/:id/edit', to: 'users#edit'

    #編集完了画面に遷移
    patch 'users/:id', to: 'users#update'

    # 投稿の削除を実施する
    delete '/users/:id', to:'users#destroy'
end

root 'users#index'を追加して、再度デプロイしたらHeroku上でも見れるようになったー!
やったね!!
スクリーンショット 2019-07-07 16.45.48.png

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

Ruby on RailsでHelloWorld

はじめに

RailsでHelloWorldする。

環境

MacOS : Mojave
Ruby : 2.3.7
Rails : 5.2.3

アプリ作成

rails new helloworld
cd helloworld

スクリーンショット 2019-07-07 16.30.07.png
スクリーンショット 2019-07-07 16.30.37.png

サーバ起動

rails server

スクリーンショット 2019-07-07 16.30.52.png

起動確認

ブラウザからいかにアクセス。
http://localhost:3000/

HelloWorld的な画面が表示されました。
スクリーンショット 2019-07-07 16.31.05.png

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

Rails で ActiveInteraction を使うメリット

はじめに

ActiveInteraction とは、rails で controller に書かれるビジネスロジックを整理するときに使えるgemです。プロダクトが大きくなるにつれて複雑になり、太った controller や model をスリムにすることができます。また、ActiveModel が名詞でわかりやすく扱えるのと同様に、ActiveInteraction は動詞で命名してわかりやすく扱うことができます。

使いかたについては、ActiveInteraction の README または、こちらの Qiitaの記事 を参考にしてみてください。

この記事では、ActiveInteraction のメリットについて書いていきます。

ActiveInteraction を使うメリット

複雑になりがちな controller を簡潔にできる

ActiveInteraction を使う一番のメリットは、複雑になった controller を簡潔にできることです。

例 : order と report モデルがあり、二つは関連を持っているとします。report は、create した時にステータスによって通知を送るべきかどうか変えたい、という要件があった場合、そのロジックを controller から分離することができます。

(簡略化して書いています)

def create
 #  ここに書くべき内容を ActiveInteraction::Base を継承している create.rb に移動できる
  outcome = Reports::Create.run(reports_create_params)

  if outcome.valid?
    redirect_to order_url(@order), notice: 'create!'
  else
    flash.now[:alert] = 'error!'
    render :new
  end
end
# create.rb
module Reports
  class Create < ActiveInteraction::Base
    object :order, class: Order
    object :account, class: Account
    string :content, default: nil
    array :images, default: []
    string :to_state, default: nil

    def execute
      report = order.reports.build(
        content: content, images: images,
        account: account, date: Time.current.to_date
      )

      ActiveRecord::Base.transaction do
        report.save!

        unless to_state&.to_sym == :accepted
          compose(Notifications::Send)
        end
      end

      report
    end
  end
end

controller がとてもシンプルで読みやすいですね。

複雑な controller は可読性が悪く、バグを生む原因にもなります。

ActiveInteraction を使うことで、controller で書くことをシンプルに保つことができ、ビジネスロジックを名前空間を使って他のところに移動させることができます。

このメリットはservice層を作ることで補うこともできるのですが、ActiveInteraction を使うことでよりシンプルにわかりやすくなります。ちなみに僕の会社では、もともとservice層にロジックを押し込んでいたこともあり、今はservice層という名前空間に ActiveInteraction::Base を継承させて利用しています。

なぜ ActiveInteraction を使うとよりわかりやすくなるのか知りたい方は、こちらの記事をご覧ください。

バグを生みにくい(見つけやすい)

二つ目のメリットは、上記した通り、バグを生みにくいということです。

ActiveInteraction は静的型付けを行います。なので、型が原因で起こるバグを潰すことができます。型が決まっているので、複数人で開発している時でもその変数に何が入っているのか一目瞭然で可読性が高いです。

# このような感じでかけます
object :purchase, class: Order
object :account, class: Account
string :content, default: nil
array :images, default: []

また、Validation をつけることができます。書き方は model と全く同じです。そもそも ActiveInteraction 内で Validation チェックすることがあまりないので利用頻度は高くないと思いますが、こちらもうまく使うことでバグの原因を潰すことができます。

ロジックのテストが描きやすい

三つ目のメリットは、ロジックだけを隔離することにより、テストが描きやすくなることです。
ActiveInteraction は一つのロジックを一つのファイルに納めるので、テストが一つのロジック毎に別れ、シンプルに保つことができます。また、テストする部分を明確にすることができます。

最後に

ActiveInteraction は README がとてもわかりやすいので、困ったことがあれば大体は README で解決できます。

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

DeviseとOmniauthでtwitter,facebook ログイン機能

はじめに

自分が実装しているときに割とハマったので書きました。
deviseでuserモデルを作成するとこから始めます。

公式ドキュメント
github-devise-wiki OmniAuth: Overview

参考資料
Devise+OmniAuthでQiita風の複数プロバイダ認証-Qiita
Rails5.2から追加された credentials.yml.enc のキホン-Qiita
[Rails]gem "OmniAuth" の脆弱性対策-Qiita
secrets.ymlや環境変数をRails 5.2のEncrypted Credentialsに移行する

gem

以下のgemを追加して bundle install します。

gemfile
gem "devise"
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'

deviseのインストール

$ rails g devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

上の4つを行ってください。

userモデルの設定

userモデルを作成

$ rails g devise User
:models/user.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
# :omniauthableを追加してください。
end

bundle install します。

omniauth用のカラムをuserモデルに追加して行きます。↓
oauth認証時にproviderの名前(facebook,twitter)、uid(providerのユーザーID)がいるみたいなので追加します。
また、image,nameも取得したいので追加します。

$ rails g migration add_columns_to_users provider uid name image
$ rake db:migrate

Twitter DeveloperとFacebook Developerのアカウント作成

すいませんが以下のサイトを見てください。詳しく書いてあります。

Twitter Developer

Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2018年9月時点の情報-Qiita

Facebook Developer

【2019年】これで完璧!Facebook Developerに登録する方法を図解つきで説明

Developerでアプリを作成

アカウントを作成したら、Developerでアプリを作成してください。

Twitter Developer

Twitter Developerでは Callback URLに
https://127.0.0.1:3000/users/auth/twitter/callback(127.0.0.1:3000にはprodction用のurl)
http://localhost:3000/users/auth/twitter/callback

この2つを記入してください。

Facebook Developer

またこのサイトを見てもらって、
【2019年】これで完璧!Facebook Developerに登録する方法を図解つきで説明

ダッシュボード下の設定→ベーシック→ウェブサイトのサイトurlに
ローカルだけで試すのであれば
http://localhost:3000

ローカルと本番環境(heroku)
自分のサイトのurl

を記入してください。

本番環境では有効なOAuthリダイレクトURIの設定が必要
facebookログイン下の設定→有効なOAuthリダイレクトURIに
https://127.0.0.1:3000/users/auth/facebook/callback(127.0.0.1:3000にはprodction用のurl)

Oauthの設定

プロバイダーの宣言

config/initializers/devise.rbでプロバイダーのkeyとsecret_keyを設定します。

config/initializers/devise.rb
Devise.setup do |config|
  config.omniauth :facebook, 'App ID', 'App Secret'
  config.omniauth :twitter, 'API key', 'API secret'
end

私の場合ローカルで繋がったのが確認出来るまでは直接書いちゃってます。

注意としてここに書いたままで、 githubなどのリモートリポジトリーにあげないこと。
上げるときは、dotenvfigarocredentials.yml.encなどで管理してください。

userモデルにメソッドを追加

models/user.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

# このから下を追加--------------------------------------------------------------
  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.name = auth.name
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20] # ランダムなパスワードを作成
      user.image = auth.info.image.gsub("_normal","") if user.provider == "twitter"
      user.image = auth.info.image.gsub("picture","picture?type=large") if user.provider == "facebook"
    end
  end
end

このメソッドはproviderand uidフィールドで既存のユーザーを見つけようとします。ユーザーが見つからない場合は、ランダムなパスワードといくつかの追加情報を使用して新しいユーザーが作成されます。 後ろにくっついているfirst_or_createメソッドにそのような機能があるみたいです。

emailはdeviseでユーザーを登録する時に必ず、必要なので追加してます。

imageについてはそのまま登録してしまうと画像が荒くなってしまいます。
twitterに関してはgsubメソッドを使用してurlの"_normai"""に置き換えてます。
facebookに関してはgsubメッソドを使用してurlの"picture""picture?type=large"に置き換えてます。

また、carrierwaveを使ってアップローダーを挟みたい場合、画像の保存先をuser.imageからuser.remote_image_urlに変更したらアップローダー経由でimageを保存できます。(ローカルでしか試していません)

コールバックの設定

どのコントローラーでOmniauthコールバックを実装するかをDeviseに伝える必要があります。

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

end

今回はcontrollers/users/omniauth_callbacksでコールバックを実装します。
なので次にusersの中にomniauth_callbacks コントローラーを作成します。

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

  def facebook
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end

  def twitter
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "Twitter") if is_navigational_format?
    else
      session["devise.twitter_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

コールバックはプロバイダと同じ名前のアクションとして実装する必要があります。(今回はtwitterとfacebook)

ここでuserモデルで作成したself.from_omniauthメソッドを使って、引数にrequest.env["omniauth.auth"]をセットしてます。
request.env["omniauth.auth"]の中に受け取ったユーザーのデータが入ってるみたいです。

persisted?メソッドでデータベースに@userのデータがあるかないかで条件分岐してます。

リンクについて

ログインへのリンクはdeviseで勝手に生成されています。
追加した:omniauthableのおかげみたい。
<%= render "devise/shared/links" %>
このコードが書いてあるログインとサインアップ画面に出てると思います。

models/user.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 # これです。
end

実際に出現しているリンクが以下のリンクです。

devise/shared/_links.html.erb
<%- if controller_name != 'sessions' %>
  <%= link_to "Log in", new_session_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
  <%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
  <%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
  <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
<% end %>

<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
  <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<% end %>

<!-- --------この部分のコードです。------------------------------------------- -->
<%- if devise_mapping.omniauthable? %>
  <%- resource_class.omniauth_providers.each do |provider| %>
    <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
  <% end %>
<% end %>
<!-- -------------ここまで----------------------------------------------------- -->

スクリーンショット 2019-07-08 8.32.45.png

適当で申し訳ないですが、こんな感じでリンクが出ます。
ここまで実装しますと、twitterとfacebookでログインが出来るようになります。

編集ページについて

deviseはデフォルトで編集時にパスワードが必要になります。
twitter、facebookでログインするとこの場合はパスワードをランダムで生成してログインしているため、パスワードを把握しておらず、編集が出来ないと思います。

そこで編集時にパスワードを入力をしないでいいように編集します。(ただし、パスワードの変更はそのページでは出来なくなります)

私もなんでこうなるのかとか曖昧ですのでこちらを読んでもらえると助かります。
参考リンク↓
github-devise-wiki How To: Allow users to edit their account without providing a password

ルートの作成

config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', registrations: 'registrations' }
# registrations: 'registrations' を追加してます。
  resources :users, only: [:index, :show]
end

コントローラーの作成

上のルートに合わせてコントローラーを作成します。(生身はサイトのコピーです)

controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  protected

  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

update_resourceメソッドをオーバーライドして、アップデート時のパスワード入力を取り除きました。
devise/registrations/edit.html.erbにcurrent_password(変更時に入力するパスワードのフォーム)が残っているため取り除きます。

devise/registrations/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true, autocomplete: "name" %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :image %><br />
    <%= f.file_field :image %>
  </div>


  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div class="field">
    <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
    <% if @minimum_password_length %>
      <br />
      <em><%= @minimum_password_length %> characters minimum</em>
    <% end %>
  </div>

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

<!-- -----------この部分を取り除きます--------------------------------------------- -->
  <div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
    <%= f.password_field :current_password, autocomplete: "current-password" %>
  </div>
<!-- -----------この部分を取り除きます--------------------------------------------- -->

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>

<%= link_to "Back", :back %>

これで完了です。このフォームではパスワードは変更出来なくなっているので、そっちも消していいと思います。

キーとシークレットキーの管理について(credentials.yml.enc)

今回はcredentials.yml.encを使って行きます。
細かいところはこちらへ↓
Rails5.2から追加された credentials.yml.enc のキホン-Qiita

$ EDITOR="vi" bin/rails credentials:edit

EDITORを指定してください。今回はvimです。
以下の記述を追加します。ご自身のkeyとserect_keyを記述してください。

facebook:
  key: "App ID"
  secret: "App Secret"
twitter:
  key: "API key"
  secret: "API secret"

credentials.yml.encファイル自体の中身が暗号化されてしまうため、参照するには以下のコマンドで入力する必要があります。

$ rails credentials:show

また、値は以下のコマンドで参照出来ます。(コンソールで試してみます)

irb(main):001:0> Rails.application.credentials.facebook
=> {:key=>App ID, :secret=>"App Secret"}
irb(main):002:0> Rails.application.credentials.facebook[:key]
=> App ID
irb(main):003:0> 

上のコードを参考にconfig/initializers/devise.rbを書き換えます。

config/initializers/devise.rb
Devise.setup do |config|
  config.omniauth :facebook, Rails.application.credentials.facebook[:key],
                             Rails.application.credentials.facebook[:secret]

  config.omniauth :twitter, Rails.application.credentials.twitter[:key],
                            Rails.application.credentials.twitter[:secret]
end

これで完了です。問題なくログインできると思います。(ローカル環境)

Prodction環境の設定(heroku)

アクセス用のkeyとsecret_keyを渡してあげる必要があるります。
参考サイト(詳しくはこちら)↓
secrets.ymlや環境変数をRails 5.2のEncrypted Credentialsに移行する

$ heroku config:set RAILS_MASTER_KEY=`cat config/master.key`

git push heroku masterを行う前に実行してください。
先にpushするとエラーが出てしまいました。

"OmniAuth" の脆弱性対策

githubにpushするとomniauthの脆弱性を発見しましたとメールがきました。
対策として以下を参考にしました。↓
[Rails]gem "OmniAuth" の脆弱性対策-Qiita

gemの追加

gemfile
gem "omniauth-rails_csrf_protection"

リンクにメソッドを宣言する

リンクにmethod: :postを加えるだけです。

devise/shared/_links.html.erb
# 上記のコード省略

<!-- --------この部分のコードです。------------------------------------------- -->
<%- if devise_mapping.omniauthable? %>
  <%- resource_class.omniauth_providers.each do |provider| %>
    <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %><br />
  <% end %>
<% end %>
<!-- -------------ここまで----------------------------------------------------- -->

その他

twitterとfacebookのアカウントが1つしかなかったため、何度も消して試してました。
以下データベースのリセットのコマンドです。

development

$ rails db:migrate:reset

prodction(heroku)

$ heroku pg:reset DATABASE

このコマンドだともう一度heroku run rails db:migrateを行う必要があります。
多分もっといいコマンドがあるはず。

最後に

これでtwitterとfacebookでログインすることができます。
まだまだ、書いている途中なので、付け加えて行きます。

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

配列

自分用忘備録

def new
@category_parent_array = ["---"]
Category.where(ancestry: nil).each do |parent|
@category_parent_array << parent.name
end
end

@category_parent_array = ["---"]
[]の中に初期値を設定している。"---"が0番となり、こんな感じで最初に表示される。
スクリーンショット 2019-07-07 14.28.15.png

@category_parent_array << parent.nameでその後追加されるもの1番以下で続く。

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

enumを使ってセレクトボックスに情報を入れる方法

1 model.rb

enum delivery_area:{
"---":0,
北海道:1,青森県:2,岩手県:3,宮城県:4,秋田県:5,山形県:6,福島県:7,
茨城県:8,栃木県:9,群馬県:10,埼玉県:11,千葉県:12,東京都:13,神奈川県:14,
新潟県:15,富山県:16,石川県:17,福井県:18,山梨県:19,長野県:20,
岐阜県:21,静岡県:22,愛知県:23,三重県:24,
滋賀県:25,京都府:26,大阪府:27,兵庫県:28,奈良県:29,和歌山県:30,
鳥取県:31,島根県:32,岡山県:33,広島県:34,山口県:35,
徳島県:36,香川県:37,愛媛県:38,高知県:39,
福岡県:40,佐賀県:41,長崎県:42,熊本県:43,大分県:44,宮崎県:45,鹿児島県:46,沖縄県:47
}

2 haml

    .listing-product-regional-original-delivery
      .listing-default__label 配送元の地域
      %span.listing-default--require 必須
      .listing-select-wrapper
        .listing-select-wrapper__box
          = f.select :delivery_area, Product.delivery_areas, {}, {class: 'listing-select-wrapper__box--select'}
          %i.fas.fa-chevron-down.listing-select-wrapper__box--arrow-down

(補足)
= f.select :今回はカラム名に合わせた名前, モデル名.今回はカラム名に合わせたenum名の複数形, {オプションが必要なら記述、今回は特にないがオプションなしを明示しなければならないので、空欄で記載}, {このセレクトボックスのクラス名}

(追記)
enumは同じkeyを使ってはいけないらしい。
<参考>https://www.changesworlds.com/blog/2017/08/how-to-use-activerecord-enum-and-how-to-avoid-duplicate-errors/

下記だと "---":0,が重複している為、ArgumentErrorが発生する。
参考URLのように_prefix: trueをつけることで解決できた。

enum delivery_method:{
"---":0,
未定:1,らくらくメルカリ便:2,ゆうメール:3,レターパック:4,普通郵便(定型、定型外):5,
クロネコヤマト:6,ゆうパック:7,クリックポスト:8,ゆうパケット:9
},_prefix: true

enum delivery_area:{
"---":0,
北海道:1,青森県:2,岩手県:3,宮城県:4,秋田県:5,山形県:6,福島県:7,
茨城県:8,栃木県:9,群馬県:10,埼玉県:11,千葉県:12,東京都:13,神奈川県:14,
新潟県:15,富山県:16,石川県:17,福井県:18,山梨県:19,長野県:20,
岐阜県:21,静岡県:22,愛知県:23,三重県:24,
滋賀県:25,京都府:26,大阪府:27,兵庫県:28,奈良県:29,和歌山県:30,
鳥取県:31,島根県:32,岡山県:33,広島県:34,山口県:35,
徳島県:36,香川県:37,愛媛県:38,高知県:39,
福岡県:40,佐賀県:41,長崎県:42,熊本県:43,大分県:44,宮崎県:45,鹿児島県:46,沖縄県:47
},_prefix: true

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

リクエストのスタブ化ができる WebMock の使い方を簡単に確認する

はじめに

外部 API のリクエストを含むテストを試したいと思ったときに、Webmock という gem でリクエストをスタブ化できることを知りました。

有名な gem なのかもしれませんが、使ったことがまだなかったので、簡単に使い方を試してみました。

前提環境

  • Mac OS X 10.14.4
  • Rails 5.2.2.1
  • ruby 2.5.5p157 (2019-03-15 revision 67260)]

試し方

今回は WebMock の使い方をすぐに確認したかったので、予め準備しておいたサンプルの Rails アプリのコンソール上で、 Webmock のメソッドの使い方を確認します。

手順

インストール

Gemfile に Webmock をインストールします。

Gemfile
gem 'webmock'

下準備

rails c でコンソールを起動したら、Webmock を利用できるよう、有効化の設定を入れます。

require 'webmock'
include WebMock::API
WebMock.enable!

こちらによって、WebMock のスタブへのリクエストに切り替わります。

README のサンプルを試す

下準備が終わったら、README を参考に、いくつかサンプルを試します。

URL のみのスタブリクエスト

以下のコードで、ダミーの URL である www.example.com へのすべてのリクエストが許可される形になります。

stub_request(:any, "www.example.com")
Net::HTTP.get("www.example.com", "/") 
#=> ""

クエリパラメータ付き URL のみのスタブリクエスト

クエリパラメータなど、変数に当たる部分は {?} という形で囲むことで使用することができます。

uri_template = Addressable::Template.new "www.example.com/users{?name}"
stub_request(:any, uri_template)

Net::HTTP.get('www.example.com', '/users?name=hoge') 
# => ""

ダミーのレスポンスを指定

リクエスト先の URL に対してダミーのレスポンスを指定できます。

stub_request(:any, "www.example.com")
  .to_return(body: "aaa", status: 200 )
Net::HTTP.get('www.example.com', '/') 
# => "aaa"

レスポンスをテキストファイルで指定

別で保存しておいたテキストファイルをレスポンスとして指定することもできます。

File.open('/tmp/response.txt', 'w') { |f| f.puts 'bbb' }

stub_request(:any, "www.example.com").
  to_return(body: File.new('/tmp/response.txt'), status: 200)

Net::HTTP.get('www.example.com', '/')
# => "bbb\n"

おわりに

こちらを応用すれば、 外部通信が伴う API クライアントが絡んだテストも、リクエストの URL さえわかればスタブ化してテストすることができそうです。

今回は試していませんが、リクエストヘッダありの POST リクエストもスタブ化できるようなので、利用の幅が広いなと感じました。

これから試してみようと思います。

参考

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

たのしいOSSコードリーディング: Let's read "cookies"?

この記事は2019年7月6日に開催されたTama Ruby会議での発表「たのしいOSSコードリーディング: Let's read "cookies"?」を詳細解説するものです。

Railsアプリケーションで使用されるcookiesメソッドを題材に、このメソッドがどのように実装されているかを読んでいきます。
読みきれなかった部分・知識が曖昧な部分が残っているため、先輩方からの技術的指摘をお待ちしています。

当日の発表資料はこちら

調査環境

  • Rails 6.0.0.rc1
  • TraceLocation 0.9.3.1

そもそもcookiesメソッドとは

cookies[:hoge] = "fuga"

Railsが用意しているメソッド。
Cookieに「hoge=fuga」を設定するときに使用。

事前知識

①Cookie

クライアントからサーバーへリクエストを送る際、HTTPリクエストヘッダはCookieヘッダを含んでいる。

クライアント?‍? →  リクエスト[Cookie: hoge=fuga]   → サーバー?‍?

Cookieヘッダの中身はこんな感じ。

NAME1=OPAQUE_STRING1; NAME2=OPAQUE_STRING2 ...

サーバーからクライアントにレスポンスを送る際、HTTPレスポンスヘッダはSet-Cookieヘッダを含んでいる。

クライアント?‍? ← レスポンス[Set-Cookie: hoge=fuga] ← サーバー?‍?

Set-Cookieヘッダの中身はこんな感じ。

NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; ...

いずれの場合も、Cookieは次のように名前と値を一つのペアとしている?

Cookieの名前=Cookieの値

(Rubyスクリプトの中で扱う際、ハッシュの形式{名前: 値}に変換したりする)

②Rackミドルウェア

Rackとは

  • 「Rack」は次のふたつの意味を持っている
    • WebアプリケーションとWebサーバーを繋ぐプロトコル
    • WebアプリケーションとWebサーバーを繋ぐライブラリ
  • Railsアプリケーションは、「Rackプロトコルを満たし、かつRackライブラリを内部で使うRackアプリケーション」

Rackミドルウェアとは

  • Webアプリケーションが持っているべき、「特定の汎用的な機能」を切り出したRackライブラリ
  • Railsアプリケーションは多くのRackミドルウェアを使用している
  • 今回見ていくActionDispatch::CookiesはRailsで使用されているRackミドルウェアのひとつ
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
 ()...

ActionDispatch::Cookiesとは?

  • Railsアプリケーションで使用されているRackミドルウェアのひとつ
  • Cookieを保存するために使う
$ rails middleware
 ()...
use ActionDispatch::Cookies # ←これ
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes

それでは早速読んでいきましょう?

cookies[:hoge] = "fuga"の全体図

trace_locationを使用すると、cookiesメソッドを呼んだ際、次のように一連の処理が走ることが確認できます。

Logged by TraceLocation gem at 2019-06-05 20:31:31 +0900
https://github.com/yhirano55/trace_location

[Tracing events] C: Call, R: Return

C actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12 [ActionController::Cookies#cookies]
  C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11 [ActionDispatch::Request#cookie_jar]
    C rack-2.0.7/lib/rack/request.rb:58 [Rack::Request::Env#fetch_header]
    R rack-2.0.7/lib/rack/request.rb:60 [Rack::Request::Env#fetch_header]
  R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:15 [ActionDispatch::Request#cookie_jar]
R actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:14 [ActionController::Cookies#cookies]
C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374 [ActionDispatch::Cookies::CookieJar#[]=]
  C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350 [ActionDispatch::Cookies::CookieJar#handle_options]
  R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:370 [ActionDispatch::Cookies::CookieJar#handle_options]
R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:392 [ActionDispatch::Cookies::CookieJar#[]=]

Result: fuga

要約するとこんな感じ

[Rails]ActionDispatch::Cookiesミドルウェアにアクセスする

[Rails][Rack]リクエストから既存のCookieヘッダを見つけて返す
             見つからなかった場合は空のCookieをセットする

[Rails]ActionDispatch::Cookiesのインスタンスを生成
     インスタンス変数を初期化する

[Rails]ActionDispatch::Cookiesのインスタンス変数に
     新しいCookieの値を追加する

[Rails]ミドルウェアがアプリケーションを返すタイミングで
     インスタンス変数の内容をSet-Cookieヘッダに書き込む(?)  ここは処理を追いきれず…

RailsとRackの処理を行ったり来たりするため、手元にソースコードがある方は確認しながら進むことをお勧めします。

ひとつずつ読んでいきましょう?

rails/actionpack/lib/action_controller/metal/cookies.rb

      7 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12#cookies

Rails側から処理を追います?
まずはActionController::Cookies#cookiesメソッドを読んでみましょう。

ActionController::Cookies#cookies

actionpack/lib/action_controller/metal/cookies.rb
 11     private
 12       def cookies
 13         request.cookie_jar
 14       end

一番最初に呼ばれる#cookiesメソッドの中身はこんな感じ、レシーバに対して#cookie_jarメソッドを呼んでいるだけ。

レシーバのrequestActionDispatch::Requestのインスタンス(※include先で定義されている)。

続いて#cookie_jarメソッドを読んでみましょう。

rails/actionpack/lib/action_dispatch/middleware/cookies.rb

      8   C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11#cookie_jar

引き続きRails?

ActionDispatch::Request#cookie_jar

actionpack/lib/action_dispatch/middleware/cookies.rb
 11     def cookie_jar
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 14       end
 15     end

#cookie_jarメソッドの中身はこんな感じ。
一行目でRackのメソッドであるfetch_header(Rack::Request::Env#fetch_header)を呼んでいます。

続いてRack::Request::Env#fetch_headerを読んでみましょう。

rack/lib/rack/request.rb

      9     C /vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/request.rb:58#fetch_header

Rack側に移ります?

Rack::Request::Env#fetch_header

rack/lib/rack/request.rb
     69       def fetch_header(name, &block)
     70         @env.fetch(name, &block)
     71       end

#fetch_headerメソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。

nameとは

引数のname"action_dispatch.cookies"

(参考)

actionpack/lib/action_dispatch/middleware/cookies.rb
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 14       end

&blockとは

引数の&blockfetch_headerに渡されているブロック
self.cookie_jar = Cookies::CookieJar.build(self, cookies)

(参考)

actionpack/lib/action_dispatch/middleware/cookies.rb
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies) 
 14       end

レシーバ@envとは

HTTPヘッダを表すハッシュ。
例えばこんなRackアプリがあった場合

['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]

真ん中の{'Content-Type' => 'text/html'}というハッシュが、Rack上では@envというインスタンス変数に入っています。

つまり…

fetch_headerは、ヘッダである@envハッシュに対して、Hash#fetchメソッドを呼ぶための処理。

すなわち

  • @env"action_dispatch.cookies"というkeyを持っていた場合:
    • ➡︎そのvalueが返る
  • @env"action_dispatch.cookies"というkeyを持っていなかった場合;
    • ➡︎ブロックself.cookie_jar = Cookies::CookieJar.build(self, cookies)が実行される

続いて、ブロックself.cookie_jar = Cookies::CookieJar.build(self, cookies)が実行された場合の処理を追います。

rails/actionpack/lib/action_dispatch/middleware/cookies.rb

self.cookie_jar =は、ActionDispatch::Request#cookie_jar=としてメソッド定義されています。
早速読んでみましょう?

ActionDispatch::Request#cookie_jar=

actionpack/lib/action_dispatch/middleware/cookies.rb
     28     def cookie_jar=(jar)
     29       set_header "action_dispatch.cookies", jar
     30     end

ここで登場する引数のjarの正体はCookies::CookieJar.build(self, cookies)ですが、この処理は後で読むので一旦パス。

二行目で、再びRackのメソッドであるset_headerを呼んでいます。

#set_headerの処理を読んでみましょう。

rack/lib/rack/request.rb

再びRackへ?

Rack::Request::Env#set_header

lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end

#fetch_headerメソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。

引数nameとは

引数のnameは"action_dispatch.cookies"

引数vとは

vは先程のjar(つまりCookies::CookieJar.build(self, cookies))

具体的には

@env["action_dispatch.cookies"] = Cookies::CookieJar.build(self, cookies)

という処理が走っています。
このとき、返り値はCookies::CookieJar.build(self, cookies)になります。

ここまでのまとめ

アプリケーションでcookiesメソッドが呼ばれると、次の2つの処理が走る。
1. @envハッシュにおける"action_dispatch.cookies"keyの存在を確認する。
2. 存在する場合はvalueを返す。
存在しない場合は新たに{"action_dispatch.cookies":
*Cookies::CookieJar.build(self, cookies)*}
というペアをつくる。

Let's read Rack::Request::Helpers#cookies?

ここからは、先ほどパスしたCookies::CookieJar.build(self, cookies)を読んでいきます。

ActionDispatch::Cookies::CookieJar.build

actionpack/lib/action_dispatch/middleware/cookies.rb
     11     def cookie_jar
     12       fetch_header("action_dispatch.cookies") do
     13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
     14       end
     15     end

↑13行目でself.cookie_jar =に渡しているCookies::CookieJar.build(self, cookies)

が、その前に

注目:eyes:

Cookies::CookieJar.build(self, cookies)

ここで引数に入れたものは何?

(self, cookies)

selfは一番最初に出てきたrequest
cookiesは、これもRackのメソッドを呼んでいます。

rack/lib/rack/request.rb

cookiesの正体を探るため、Rack::Request::Helpers#cookiesメソッドを読んでいきましょう?

Rack::Request::Helpers#cookies

lib/rack/request.rb
    215       def cookies
    216         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
    217           set_header(k, {})
    218         end
    219         string = get_header HTTP_COOKIE
    220 
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
    222         hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
    223         set_header(RACK_REQUEST_COOKIE_STRING, string)
    224         hash
    225       end

#cookiesの中身はこんな感じ。

一行づつ

lib/rack/request.rb
    216         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
    217           set_header(k, {})
    218         end

fatch_headerは先程と同じメソッドで、@envハッシュに対してRACK_REQUEST_COOKIE_HASHkeyをfetchします。

  • keyがあった場合
    • ➡️valueを返す
  • keyがなかった場合
    • ➡️これも先程登場したset_headerメソッドを使用して、 @envハッシュにRACK_REQUEST_COOKIE_HASH: {}というペアを追加する
    • 返り値は空の{}

(参考)

lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end

いずれの場合も、返り値をhashに代入しています。

RACK_REQUEST_COOKIE_HASHとは?

ここで登場する、RACK_REQUEST_COOKIE_HASHの正体は、rack.request.cookie_hash
Rackがサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)

example.rb
{"_todo_session"=>"BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg==--c34c5fc6de928cde391cccd2b710547c7aab1d06"}

(↑の例はたまたま下記の記事中で見つけたものです)
Rails で捕捉されない例外が発生したらメールを送る

つづきまして

lib/rack/request.rb
    219         string = get_header HTTP_COOKIE

HTTP_COOKIEを引数として、get_headerメソッドを呼んでいます。
get_headerRack::Request::Env#get_headerで、定義は以下の通り。

lib/rack/request.rb
    63          def get_header(name)
    64            @env[name]
    65          end

@envハッシュからHTTP_COOKIEkeyを探し、valueを返し、返り値がstringに代入されます。
ただし、初めてCookieを使用する場合は@envハッシュにHTTP_COOKIEkeyが見つからないため、nilが返ります。

HTTP_COOKIEとは?

ここで登場するHTTP_COOKIEの正体は、リクエストメッセージに含まれるメタ変数で、Cookieヘッダを返しています。
中身はこんな感じ(Cookieの名前と値を=で結ぶことでペアとして表現している)
(例)

example.rb _todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06

(↑の例も先ほどの記事よりお借りしました)

※メタ変数について:
参照

例えば HTTP というプロトコルにおいては、 HTTP_USER_AGENT という名前のメタ変数が HTTP 要求メッセージの User-Agent: 欄の値を提供することになっています

つづき

初めてCookieを使用する場合、ここまでで

  • hash{}
  • stringget_header HTTP_COOKIEの返り値(初めてCookieを使用する場合はnil

が代入されていることになります。

lib/rack/request.rb
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)

RACK_REQUEST_COOKIE_STRINGを引数に、Rack::Request::Env#get_headerメソッドを呼んでstringと比較。

(参考)

lib/rack/request.rb
    63          def get_header(name)
    64            @env[name]
    65          end

RACK_REQUEST_COOKIE_STRINGとは?

ここで登場するRACK_REQUEST_COOKIE_STRINGの正体はrack.request.cookie_stringで、こちらもサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)

example.rb
_todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06

(御多分に洩れず↑の例も先ほどの記事より)

HTTP_COOKIEと同じですね!

つづき

lib/rack/request.rb
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)

==の場合はここでreturn hash

なぜ?

次行以降はリクエストヘッダからやって来たhash(初めてのCookieを使用する場合は空の{})をHTTP_COOKIEで更新する処理となっています。
ハッシュの中身が変更されていない場合、置き換える必要がないため。

初めてCookieを使用する場合、stringget_header(RACK_REQUEST_COOKIE_STRING)の返り値はいずれもnilのため、ここでcookiesメソッドの処理はhash(空の{})を返して終了します。

つづき(すでにCookieが存在する場合のみ)

lib/rack/request.rb
    222         hash.replace Utils.parse_cookies_header string

ここでは、
Utils.parse_cookies_header(string)の返り値{ Cookieの名前: Cookieの値 }で、hash(fetch_header(RACK_REQUEST_COOKIE_HASH)の返り値)をreplaceしています。

Hash#replace

Utils.parse_cookies_headerRack::Utilsのメソッドで、
詳しくは読みませんが、
string(Cookieの名前=Cookieの値)を{Cookieの名前: Cookieの値}
のようなハッシュに変換する役割を担っています。

(参考)

lib/rack/utils.rb
    209     def parse_cookies_header(header)
    215       cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
    216       cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v }
    217     end

(↑each_with_objectを使用して{}に名前と値を代入している)

つづき(すでにCookieが存在する場合のみ)

lib/rack/request.rb
    223         set_header(RACK_REQUEST_COOKIE_STRING, string)
    224         hash
    225       end

すなわち、ここで

@env[RACK_REQUEST_COOKIE_STRING] = string

という処理を実行。

(参考)

lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end

最後にhash({Cookieの名前: Cookieの値}) を返しています。

ここまでのまとめ

メソッドcookiesを呼ぶとき

  • 初めてCookieを使用する場合: ヘッダ(@env)に{RACK_REQUEST_COOKIE_HASH: {} }が追加される
    • 最終的な返り値は空の{}
  • すでにCookieが存在する場合: ヘッダ(@env)に{RACK_REQUEST_COOKIE_STRING: リクエストメッセージのCookieヘッダ}が追加される
    • 最終的な返り値は{Cookieの名前: Cookieの値}

改めましてActionDispatch::Cookies::CookieJar.build

actionpack/lib/action_dispatch/middleware/cookies.rb

お待たせしました。Cookies::CookieJar.buildを読んでいきましょう。
再びRailsへ?

ActionDispatch::Cookies::CookieJar.build

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    289       def self.build(req, cookies)
    290         new(req).tap do |jar|
    291           jar.update(cookies)
    292         end
    293       end

Cookies::CookieJar.buildの中身はこんな感じ。

tapを使用して処理をチェーンしているのは、最終的にできあがったCookies::CookieJarインスタンスを返すためではないかと考えられます。
tapを挟まない場合、返り値がjar.updateの実行結果になる)

ここで登場するオブジェクトを確認しましょう。

reqとは

引数のreqは一番最初のrequestActionDispatch::Requestのインスタンス)

cookiesとは

cookiesは先ほど読んだRack::Request::Helpers#cookiesの返り値({Cookieの名前: Cookieの値}あるいは空の{})

Cookies::CookieJar.buildは何をやっているのか

ここでの役割は2つ

  • Cookies::CoookieJarのインスタンスを作る
  • できたインスタンスに対してCookies::CookieJar#updateメソッドを呼ぶ

順番に処理を読んでいきます。

new(req)

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    289       def self.build(req, cookies)
    290         new(req).tap do |jar| # ←
    291           jar.update(cookies)
    292         end
    293       end

new(req)(インスタンス化)すると、initializeが呼ばれます。

actionpack/lib/action_dispatch/middleware/cookies.rb
    295       attr_reader :request
    296 
    297       def initialize(request)
    298         @set_cookies = {}
    299         @delete_cookies = {}
    300         @request = request
    301         @cookies = {}
    302         @committed = false
    303       end

ActionDispatch::Cookies::CookieJar#initializeは各インスタンス変数を初期化するだけの処理。

@requestには引数で渡されている一番最初のrequestが入ります。
attr_readerによってメソッドとしてアクセスできるようになります。

つづいてtap以下の処理

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    290         new(req).tap do |jar|
    291           jar.update(cookies) # ←
    292         end

tapブロックの中で実行されているupdateメソッドはこちら

actionpack/lib/action_dispatch/middleware/cookies.rb
    334       def update(other_hash)
    335         @cookies.update other_hash.stringify_keys
    336         self
    337       end

引数other_hashとは

引数other_hashは先ほどのcookies、つまり{Cookieの名前: Cookieの値}もしくは空の{}

@cookiesとは

@cookiesinitializeの中で初期化したこの部分

    301         @cookies = {}

ここでは、この空の{}に対してupdateメソッドを呼んでいます。
Hash#updateメソッドはHash#merge!のエイリアス。

処理を実行すると、こうなります。

@cookies = {'Cookieの名前': 'Cookieの値'}

(other_hashが空の{}の場合は、何も起きない)

その後、self (=jar、つまりCookies::CookieJar.buildでできたインスタンス)を返しています。

(余談)
↑の方でtapを使った理由を「インスタンスを返すため」と記述しているのですが、ここでもインスタンス自身を返しているため、どちらか片方の処理で良いのではという疑惑あり…PRチャンス?

また、先述の通りこのインスタンスは

actionpack/lib/action_dispatch/middleware/cookies.rb
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)

ここでself.cookie_jar=メソッドに引数として渡されて、

actionpack/lib/action_dispatch/middleware/cookies.rb
     28     def cookie_jar=(jar)
     29       set_header "action_dispatch.cookies", jar
     30     end

ここで@env = {"action_dispatch.cookies": できあがったインスタンス}としてヘッダーに追加されます。

ここまでのまとめ

アプリケーションからcookiesメソッドを呼ぶと、結果的には次のようなリクエストヘッダができる

@env = {
  "action_dispatch.cookies": Cookies::CookieJarのインスタンス},
  # 初めてCookieを使用する場合
  RACK_REQUEST_COOKIE_HASH: {},
  # すでにCookieが存在する場合
  RACK_REQUEST_COOKIE_STRING: リクエストメッセージから受け取ったCookieヘッダ
}

最後に[]=

cookies[:hoge] = :fuga

cookiesに値を代入する処理を見ていきます?

actionpack/lib/action_dispatch/middleware/cookies.rb

14 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374#[]=

ここからはRailsのお話?
#[]=メソッドを読んでいきましょう。

ActionDispatch::Cookies::CookieJar#[]=

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    372       # Sets the cookie named +name+. The second argument may be the cookie's
    373       # value or a hash of options as documented above.
    374       def []=(name, options)
    375         if options.is_a?(Hash)
    376           options.symbolize_keys!
    377           value = options[:value]
    378         else
    379           value = options
    380           options = { value: value }
    381         end
    382 
    383         handle_options(options)
    384 
    385         if @cookies[name.to_s] != value || options[:expires]
    386           @cookies[name.to_s] = value
    387           @set_cookies[name.to_s] = options
    388           @delete_cookies.delete(name.to_s)
    389         end
    390 
    391         value
    392       end

ActionDispatch::Cookies::CookieJar#[]=の中身はこんな感じ。

一行ずつ

actionpack/lib/action_dispatch/middleware/cookies.rb
    374       def []=(name, options)

これを定義すると、

[:name] = options

という形で値を代入できるようになります。

それではここで、Railsアプリからcookies[]= に渡せる値を確認しましょう?

Railsガイド

cookie[:hoge] = "fuga"のように文字列を渡すことが多いと思いますが、オプションを細かく設定する場合、右辺にはハッシュを渡すこともできます。

cookies[:hoge] = {
  value:      'Cookieの値',
  expires:    'Cookieの有効期限',
  path:       'Cookieが適用されるパス(デフォルトは/)',
  domain:     'Cookieが適用されるドメイン',
  tld_length: 'domain: :allの時、TLDの一部として解釈される短い(3文字以下の)ドメインを使用するときに、TLDの長さを明示的に設定',
  secure:     '暗号化通信のみを有効化(デフォルトはfalse)',
  httponly:   'スクリプト経由もしくはHTTP通信のみを有効化(デフォルトはfalse)'
}

これを踏まえて、引き続き処理を読んでいきます。

①引数optionsがハッシュの場合

actionpack/lib/action_dispatch/middleware/cookies.rb
    375         if options.is_a?(Hash)
    376           options.symbolize_keys!
    377           value = options[:value]
    378         else

cookies[:hoge] = {"value" => "fuga}の場合、symbolize_keys!{:value => "fuga"}に変換。

変数value"fuga"を代入。

②引数optionsがハッシュ以外の場合

actionpack/lib/action_dispatch/middleware/cookies.rb
    378         else
    379           value = options
    380           options = { value: value }
    381         end

cookies[:hoge] = "fuga"の場合、変数value"fuga"を代入。

options{value: "fuga"}を再代入。

今の状態

①②いずれの場合も、

  • options{value: "fuga"}
  • valuefuga

となっている。

つづき

actionpack/lib/action_dispatch/middleware/cookies.rb
    383         handle_options(options)

同じクラスのhandle_options(options)を呼びます。

ActionDispatch::Cookies::CookieJar#handle_options

handle_optionsoptionsをいい感じにするためのメソッドです。

actionpack/lib/action_dispatch/middleware/cookies.rb
    350       def handle_options(options) # :nodoc:
    351         if options[:expires].respond_to?(:from_now)
    352           options[:expires] = options[:expires].from_now
    353         end
    354 
    355         options[:path] ||= "/"
    356 
    357         if options[:domain] == :all || options[:domain] == "all"
    358           # If there is a provided tld length then we use it otherwise default domain regexp.
    359           domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
    360 
    361           # If host is not ip and matches domain regexp.
    362           # (ip confirms to domain regexp so we explicitly check for ip)
    363           options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
    364             ".#{$&}"
    365           end
    366         elsif options[:domain].is_a? Array
    367           # If host matches one of the supplied domains without a dot in front of it.
    368           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
    369         end
    370       end

中身はこんな感じ。

一行ずつ

actionpack/lib/action_dispatch/middleware/cookies.rb
    351         if options[:expires].respond_to?(:from_now)
    352           options[:expires] = options[:expires].from_now
    353         end

cookiesメソッドに有効期限を設定した場合(例:1.week)、from_nowで具体的な日時に変換してoptions[:expires]に再代入。
from_nowsinceのエイリアス。

actionpack/lib/action_dispatch/middleware/cookies.rb
    355         options[:path] ||= "/"

cookiesメソッドにパスが指定されていない場合、ルートパスを設定(デフォルト値として)

actionpack/lib/action_dispatch/middleware/cookies.rb
    357         if options[:domain] == :all || options[:domain] == "all"
    358           # If there is a provided tld length then we use it otherwise default domain regexp.
    359           domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
    360 
    361           # If host is not ip and matches domain regexp.
    362           # (ip confirms to domain regexp so we explicitly check for ip)
    363           options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
    364             ".#{$&}"
    365           end
    366         elsif options[:domain].is_a? Array
    367           # If host matches one of the supplied domains without a dot in front of it.
    368           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
    369         end
    370       end

誰か正規表現に強い方…………

  • [:domain]オプションが:allor"all"の場合
    • [:tld_length]が指定されている場合は/([^.]+\.?){#{options[:tld_length]}}$/
    • そうでない場合はDOMAIN_REGEXP

を変数domain_regexpに代入
(※DOMAIN_REGEXP/[^.]*\.([^.]*|..\...|...\...)$/)

リクエストメッセージのホストが/^[\d.]+$/に一致しない場合&&変数domain_regexpに一致する場合はoptions[:domain]".#{$&}"で置き換える

  • [:domain]オプションが配列の場合
    • options[:domain]を、リクエストメッセージのホストが含まれる要素で置き換える

ここでやったこと

  • 有効期限を具体的な日時に変換
  • パスの設定がない場合にデフォルト値を設定
  • ドメインをいい感じに再代入(?)

ActionDispatch::Cookies::CookieJar#[]=のつづき

今の状態

①②いずれの場合も、
- options{value: 'fuga'}
- valuefuga

となっています。

ActionDispatch::Cookies::CookieJar#[]=のつづき

actionpack/lib/action_dispatch/middleware/cookies.rb
    385         if @cookies[name.to_s] != value || options[:expires]
    386           @cookies[name.to_s] = value
    387           @set_cookies[name.to_s] = options
    388           @delete_cookies.delete(name.to_s)
    389         end
    390 
    391         value
    392       end

@cookies["hoge"]のvalueが"fuga"でない場合(つまり値が変わっている場合)、もしくは有効期限の設定がない場合に、

  • @cookies["hoge"]"fuga"を代入
  • @set_cookiesにoption{value: 'fuga'}を代入
  • @delete_cookiesから[:hoge]を削除する

をして、最後に"fuga"を返します。

最後に

trace_locationでログに残っていたのはここまでですが、
先述の通りActionDispatch::Cookiescallメソッドを持ったRackミドルウェアとして実装されており、
Railsアプリケーションから使用されています。

$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies # ←ここ 
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes

このことから、Railsアプリケーションにリクエストがあった際、Rackアプリとして次の処理が呼ばれることがわかります。

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    637     def initialize(app)
    638       @app = app
    639     end
    640 
    641     def call(env)
    642       request = ActionDispatch::Request.new env
    643 
    644       status, headers, body = @app.call(env)
    645 
    646       if request.have_cookie_jar?
    647         cookie_jar = request.cookie_jar
    648         unless cookie_jar.committed?
    649           cookie_jar.write(headers)
    650           if headers[HTTP_HEADER].respond_to?(:join)
    651             headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
    652           end
    653         end
    654       end
    655 
    656       [status, headers, body]
    657     end

中身は詳しく追っていきませんが、

actionpack/lib/action_dispatch/middleware/cookies.rb
    647         cookie_jar = request.cookie_jar
    648         unless cookie_jar.committed?
    649           cookie_jar.write(headers)
    650           if headers[HTTP_HEADER].respond_to?(:join)
    651             headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
    652           end
    653         end

この辺りでヘッダーへの書き込みが行われていることが確認できました。

今回追うことができなかった処理・残された疑問点

  • "rack.request.cookie_hash"がヘッダーに書き込まれるタイミング
  • Cookies::CookieJar#writeCookies::CookieJar#make_set_cookie_headerから、最終的に呼ばれる::Rack::Utils.add_cookie_to_header(↑の資料では未出)
    • ヘッダーに書き込む文字列の成形を行なっていることが確認できるが、その後実際にヘッダーに書き込まれるタイミング

おつかれさまでした?

参考資料

おまけ

TraceLocationで作成したログは次の通りです(マークダウン形式)
Generated by trace_location at 2019-06-05 20:28:50 +0900


actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12

ActionController::Cookies#cookies
def cookies
  request.cookie_jar
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8



actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11

ActionDispatch::Request#cookie_jar
def cookie_jar
  fetch_header("action_dispatch.cookies") do
    self.cookie_jar = Cookies::CookieJar.build(self, cookies)
  end
end
# called from actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:13



rack-2.0.7/lib/rack/request.rb:58

Rack::Request::Env#fetch_header
def fetch_header(name, &block)
  @env.fetch(name, &block)
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:12



actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374

ActionDispatch::Cookies::CookieJar#[]=
def []=(name, options)
  if options.is_a?(Hash)
    options.symbolize_keys!
    value = options[:value]
  else
    value = options
    options = { value: value }
  end

  handle_options(options)

  if @cookies[name.to_s] != value || options[:expires]
    @cookies[name.to_s] = value
    @set_cookies[name.to_s] = options
    @delete_cookies.delete(name.to_s)
  end

  value
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8



actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350

ActionDispatch::Cookies::CookieJar#handle_options
def handle_options(options) # :nodoc:
  if options[:expires].respond_to?(:from_now)
    options[:expires] = options[:expires].from_now
  end

  options[:path] ||= "/"

  if options[:domain] == :all || options[:domain] == "all"
    # If there is a provided tld length then we use it otherwise default domain regexp.
    domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP

    # If host is not ip and matches domain regexp.
    # (ip confirms to domain regexp so we explicitly check for ip)
    options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
      ".#{$&}"
    end
  elsif options[:domain].is_a? Array
    # If host matches one of the supplied domains without a dot in front of it.
    options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
  end
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:383

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

データベースに保存されている時刻とビューに表示される時刻が違う

問題:データベースの値とビューに表示される時刻が違う

仮説

1.DateTime.parse(string)で生成されたDateTimeクラスのインスタンスは協定世界時になる
2.datetime型のカラムに保存した時にJST時間に修正される

解決

1.DateTime.parse(string)で生成されるインスタンスはUTCになるは正しかったぽい参考記事

parseはUTCのタイムゾーンを使う。(環境変数のタイムゾーンを無視する)
l DateTime.parse(str)
=> 2015/01/01 00:00:00, +00:00, DateTime

2.MysqlのDATETIME型はActiveSupport::TimeWithZoneクラスであるので、レコードを保存する時に内部でこんな感じの処理が行われる、だと思う。

time = DateTime.parse(string)
=> Tue, 09 Jul 2019 12:00:00 +0000

Time.zone.parse(time.to_s)
=> Tue, 09 Jul 2019 21:00:00 JST +09:00

Time.zone.parse(time.to_s).class
=> ActiveSupport::TimeWithZone

Time.zone.parse(time.to_s).strftime("%Y年%-m月%-d日 %-H時%-M分")
=> "2019年7月9日 21時0分"
> string
=> "201907091200"

> Time.zone.parse(string)
=> Tue, 09 Jul 2019 12:00:00 JST +09:00

#DateTime.parseした場合
> DateTime.parse(string)
=> Tue, 09 Jul 2019 12:00:00 +0000 //UTCで返ってくる

> @event.deadline_date
=> Tue, 09 Jul 2019 21:00:00 JST +09:00

> @event.deadline_date.class
=> ActiveSupport::TimeWithZone

#Time.parseした場合
> Time.parse(string)
=> 2019-07-09 12:00:00 +0900 //JST(アプリケーションで設定したタイムゾーン)で返ってくる

> @event.deadline_date
=> Tue, 09 Jul 2019 12:00:00 JST +09:00

> @event.deadline_date.class
=> ActiveSupport::TimeWithZone

仮説

データベースに保存される時にDateTimeクラスからTimeWithZoneクラスに変換される

> string
=> "201907060600"

> DateTime.parse(string)
=> Sat, 06 Jul 2019 06:00:00 +0000

> Time.zone.parse(DateTime.parse(string).to_s)
=> Sat, 06 Jul 2019 15:00:00 JST +09:00

でも違うのか?

DateTimeクラスのデータをデータベースに保存するときに保存先のデータ型に変換されて保存されると思っていたけれど、データベースには 2019-07-06 06:00:00 で格納されていた。(画像3行目)
スクリーンショット 2019-07-07 10.41.24.png
でも引っ張り出すとTimeWithZone型でJSTの時刻になっていた。

> @event.deadline_date
=> Sat, 06 Jul 2019 15:00:00 JST +09:00

> @event.deadline_date.class
=> ActiveSupport::TimeWithZone

> @event.deadline_date.strftime("%Y年%-m月%-d日 %-H時%-M分")
=> "2019年7月6日 15時0分"

解決

データベースに保存された時には実際にデータ型への変換が行われるが、UTCの時刻で表示されている
アプリケーション側のタイムゾーンをtokyoつまりJSTに設定している場合はTime.parseを使ったほうがいいのかもしれない

ご指摘ください

7月からエンジニアになりました
間違っているところがありましたらご指摘ください
時間ある時に修正します
何卒よろしくお願いします

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

Nuxt.js + Rails(API) on DockerのHello Worldするべ!

Goal

友人からNuxt.jsいいよーと言われたのでひとまず触ってみるかというノリでNuxt.js + Rails(API) on DockerのHello Worldをめざします!

この記事のゴールはDocker上でフロントエンドとしてNuxt.js、バックエンドとしてRailsが連携しあってRailsのscaffold的にUserのCRUDができることです。
図にすると以下のような感じです。

Untitled Diagram (2).png

Table of contents

  1. Dockerコンテナの準備
  2. Nuxt.jsのHello world
  3. Rails(API)のHello world
  4. Nuxt.js + Rails(API)のHello world

Dockerやdocker-composeはすでにインストール済みの前提でいきます!

1. Dockerコンテナの準備

まずはNuxt.jsやRailsをDocker上で動作させるためのファイルの準備をしていきます。
Nuxt.jsは「docker で nuxt.js を開発環境を建てるだけ - Qiita」を参考にさせていただきました。
Rails(API)については「Rails on Docker(alpine)でAPIコンテナをつくってみた - Qiita」にて記事投稿しております。

詳細については各記事をご参考いただければ幸いですが、最終的なアウトプットは以下のようなものです。

DirectoryStructure
/
|--front/
|    |--Dockerfile
|--back/
|    |--Dockerfile
|    |--Gemfile
|    |--Gemfile.lock #空ファイル
|--docker-compose.yml
front/Dockerfile
FROM node:12.5.0-alpine

ENV HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo 

WORKDIR ${HOME}

RUN apk update && \
    apk upgrade && \
    npm install -g npm && \
    npm install -g vue-cli

ENV HOST 0.0.0.0
EXPOSE 8080
back/Dockerfile
FROM ruby:2.6.3-alpine3.10

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/app" \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo

WORKDIR ${HOME}

ADD Gemfile ${HOME}/Gemfile
ADD Gemfile.lock ${HOME}/Gemfile.lock

RUN apk update && \
    apk upgrade && \
    apk add --update --no-cache ${RUNTIME_PACKAGES} && \
    apk add --update --virtual build-dependencies --no-cache ${DEV_PACKAGES} && \
    bundle install -j4 && \
    apk del build-dependencies

ADD . ${HOME}

CMD ["rails", "server", "-b", "0.0.0.0"]
back/Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5'
docker-compose.yml
version: "3"

services:
  db:
    container_name: sample_db
    image: postgres:11.4-alpine
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./back/tmp/db:/var/lib/postgresql/data

  back:
    container_name: sample_back
    build: back/
    volumes:
      - ./back:/app
    depends_on:
      - db
    ports:
      - 3000:3000

  front:
    container_name: sample_front
    build: front/
    command: npm run dev
    volumes:
      - ./front:/app
    ports:
      - 8080:3000

ここまでできたらbuildしてimageを作成しましょう。

$ docker-compose build

2. Nuxt.jsのHello world

imageができあがったらまずはNuxt.jsのアプリを作っていきます。

$ docker-compose run --rm front npx create-nuxt-app

? Project name                   --> sample_app  # アプリ名
? Project description            --> sample_app  # アプリの説明
? Author name                    --> me          # アプリの作成者
? Choose the package manager     --> Npm
? Choose UI framework            --> None
? Choose custom server framework --> None
? Choose Nuxt.js modules         --> Axios
? Choose linting tools           --> -
? Choose test framework          --> None
? Choose rendering mode          --> Universal (SSR)

Nuxtアプリが作成できたらアクセスできるか確認しときます。

$ docker-compose up front

http://localhost:8080にアクセスして以下のようなページにアクセスできればNuxt.jsのHello world完了です!

image.png

(参考)Nuxt.jsではホットリローディングというファイルの変更を自動で反映してくれる機能を有効にすることができます。npm run devコマンドでホットリローディングが有効になると公式で説明されていますが何やらうまくいくときといかない時がありました...うまくいかない場合は「IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話 - エンジニアの卵の成長日記」を参考に以下のような設定を書き加えることでホットリロードされるようになりました。

front/nuxt.config.js
export default {
// 省略
  watchers: {
    webpack: {
      poll: true
    }
  }
// 省略
}

3. Rails(API)のHello world

まずRailsアプリを作成しましょう。--apiをオプションにつけることでAPIモードに不要なもの、例えばViewなどが含まれないようにrails newすることができます。

$ docker-compose run --rm back rails new . -f -d postgresql --api

Railsアプリが作成されたらDB接続の設定をします。

back/config/database.yml
##### 省略
default: &default
  adapter: postgresql
  encoding: unicode
  host: db            # add
  username: postgres  # add
  password:           # add
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 
##### 省略

DBの設定が完了したらDBを作成します。

$ docker-compose build back
$ docker-compose run --rm back rails db:create

DBの作成が完了したら、scaffoldでAPIを作ってみます。name属性をもつUserモデルを作ります。

$ docker-compose run --rm back rails g scaffold user name:string
$ docker-compose run --rm back rails db:migrate

Hello worldとして、testという名前のユーザーをAPIリクエストで作成してみます。

Rails APIでどのエンドポイントに何メソッドでリクエストすればいいかはrails routesコマンドで調べるとわかりやすいです。

$ docker-compose run --rm back rails routes
Prefix Verb   URI Pattern            Controller#Action
users  GET    /users(.:format)       users#index
       POST   /users(.:format)       users#create
user   GET    /users/:id(.:format)   users#show
       PATCH  /users/:id(.:format)   users#update
       PUT    /users/:id(.:format)   users#update
       DELETE /users/:id(.:format)   users#destroy

詳しい説明は省きますが、HTTPメソッド、URLパターン、アクションの関係性がわかるので、
ユーザーを作成する場合は「/usersにPOSTリクエスト」、特定のIDのユーザーの情報を取得する場合は「/users/:idにGETリクエスト」ということがわかります。
これを踏まえてcurlリクエストしてユーザー作成をしてきます。

$ docker-compose up -d back
$ curl -X POST http://localhost:3000/users -d 'user[name]=test'
$ curl http://localhost:3000/users/1
{"id":1,"name":"test","created_at":"2019-07-04T14:40:49.443Z","updated_at":"2019-07-04T14:40:49.443Z"}

これでRails APIのHello world完了です!

コンテナは停止しておきましょう。

$ docker-compose down

4. Nuxt.js + Rails(API)のHello world

さて、実際にはNuxt.jsのコンテナからRails(API)のコンテナにリクエストを流すので、Rails(API)のポートを外部に公開する必要はありません。誰からでもAPIのリクエストを受け取ってしまう状態はセキュリティ的にもよろしくありませんのでbackコンテナの外部公開ポートを削除しておきます。

docker-compose.yml
##### 省略
  back:
    container_name: sample_back
    build: back/
    volumes:
      - ./back:/app
    depends_on:
      - db
#   ports:        # delete
#     - 3000:3000 # delete
##### 省略

続いてNuxt.jsをいじっていきます。Hello worldでやりたいことは、

  1. http://localhost:8080/users/:idにアクセスして「Hello, (User.name)」と表示させたい
  2. http://localhost:8080/users/newにアクセスしてユーザーを追加したい

といったところにします。

4-1. http://localhost:8080/users/:idにアクセスして「Hello, (User.name)」と表示させたい

まずはhttp://localhost:8080/users/:idにアクセスした時にルーティングされるページを作成します。

$ mkdir front/pages/users
$ touch front/pages/users/_id.vue
front/pages/users/_id.vue
<template>
  <h1>Hello, {{ name }}</h1>
</template>

<script>
export default {
  asyncData({ $axios, params }) {
    return $axios.$get(`http://back:3000/users/${params.id}`)
      .then((res) => {
        return { name: res.name }
      })
  }
}
</script>

$axios.$getでGETメソッドでAPIをリクエストしています。リクエスト先はhttp://back:3000/users/${params.id}としていますが、backはbackコンテナを意味していますので、Railsアプリが入ったコンテナの/user/${params.id}にGETリクエストを飛ばしていることになります。
レスポンスの値からnameを変数として取り出し、template内の{{ name }}に入れます。

この状態でコンテナを起動してhttp://localhost:8080/users/1にアクセスすると、Rails(API)のHello worldで作成したtestユーザーの情報が取得できています。
が、Backコンテナが立ち上がっていないといけない状態になったのでdepends_onしておきましょう。

docker-compose.yml
##### 省略
  front:
    container_name: sample_front
    build: front/
    command: npm run dev
    volumes:
      - ./front:/app
    ports:
      - 8080:3000
    depends_on:
      - back
##### 省略
$ docker-compose up

image.png

4-2. http://localhost:8080/users/newにアクセスしてユーザーを追加したい

続きましてユーザーの新規登録です。こちらはPOSTリクエストしてあげることで実現できます。

先ほどと同様にhttp://localhost:8080/users/newにアクセスしたときに表示されるページを作っていきます。
このページでは、Nameを入力してsubmitするとRails APIの方にPOSTリクエストを飛ばしてUserを新規登録できるようにしたいと思います。新規登録したらそのUserのHelloページ(4-1で作成)にページ遷移するようにしましょー。

まずはじめに、Rails APIにリクエストを飛ばせるようにconfigをいじっていきます。

front/plugins/axios.js
export default function({ $axios, redirect }) {
    $axios.setToken('access_token')

    $axios.onResponse(config => {
        $axios.setHeader('Access-Control-Allow-Origin', 'http://back:3000')
    })
}
front/nuxt.config.js
export default {
// 省略
  plugins: [
    'plugins/axios'
  ],
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
    proxy: true
  },
  proxy: {
    '/api/': { target: 'http://back:3000', pathRewrite: { '^/api/': '/' } }
  },
// 省略
}

ここらへんの設定をしないとCORSエラーってのがおきちゃう。すごくつまった。
Nuxt.jsのメソッド内で外部APIを叩くとcorsエラーが起きる - Qiita」「nuxt.js で axios から外部APIを叩くとCORSエラーを解決 - Qiita」の記事を参考にしました!
4-1で実施したasyncDataはSSRなので不要なようですが、通常メソッド内でリクエストをしたい場合は信頼するドメインへのリクエストのみを許可する必要があるみたいですね。

さて、上記の設定が終わったら実際にページを作っていきます。

front/pages/users/new.vue
<template>
  <section>
    <div>
      <h1>New user</h1>
      <form @submit.prevent="post">
        <label for="name">Name: </label>
        <input id="name" v-model="name" type="text" name="name" />
        <button type="submit">submit</button>
      </form>
    </div>
  </section>
</template>

<script>
export default {
  data() {
    return {
      name: ''
    }
  },
  methods: {
    post() {
      this.$axios.post(
        '/api/users',
        {
          name: this.name
        }
      ).then((res) => {
        this.$router.push(`${res.data.id}`)
      })
    }
  }
}
</script>

このページ(http://localhost:8080/users/new)にアクセスすると下のような画面が出てきます。
image.png

POSTリクエストするあたりを説明します!

5行目:<form @submit.prevent="post">
formをレンダリングしてますが、@submit.preventでsubmit時に22行目で定義しているpost()メソッドを呼び出してます。

22行目〜31行目:POSTリクエスト
大まかに形としては

this.$axios.post(url, data).then((res) => {成功した後の動作})

という感じです。
urlには先ほどproxyで定義した/api/を用いて/api/usersを指定します。これでhttp://back:3000/usersにリクエストすることになります。
dataにはリクエストデータを記載します。今回はform内で入力しているv-model="name"の値をリクエストしたいので、{ name: this.name }としてます。
成功した後の動作としては登録したUserのHelloページへ遷移するとしてました。ページ遷移はメソッド内の場合はthis.$router.push(パス)でできるので、レスポンスデータから登録されたidを取得して、

this.$router.push(`${res.data.id}`)

とすることでPOSTリクエストに成功したときに作成したユーザーのHelloページへ自動遷移されます。

ここまで作成したら、Helloページ側のGETリクエスト先も/api/を使った記述に変更しておきます。これしないとthis.$router.push時にリクエスト失敗しちゃいました。

front/pages/users/_id.vue
// 省略
-    return $axios.$get(`http://back:3000/users/${params.id}`)
+    return $axios.$get(`/api/users/${params.id}`)
// 省略

ここまでで完成です。実際にhttp://localhost:8080/newにアクセスしてみてNameを入力しsubmitしてみましょう。

$ docker-compose up

入力したNameの「Hello, xxxxx」の画面に遷移したらNuxt.js + Rails(API)のHello worldは成功です!

Afterword

Nuxt.js初体験、Vue.jsとかも触ったことがなかったので結構苦戦をしいられました...
特にaxiosのPOSTリクエストには...かなりの時間を...うぅ...
同じくHello worldに苦しむ方の助けになれば幸いです!
引き続き勉強していかねば〜。

Reference

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

GlobalIDでセレクトボックスのグループ化(polymorphic)

Globalid

rails/globalid

irb(main):001:0> dog_gid = Dog.find(1).to_global_id
=> #<GlobalID:0x00007fa353e34588 @uri=#<URI::GID gid://global-id/Dog/1>>

irb(main):002:0> dog_gid.uri
=> #<URI::GID gid://global-id/Dog/1

irb(main):003:0> dog_gid.to_s
=> "gid://global-id/Dog/1"

irb(main):005:0> GlobalID::Locator.locate dog_gid
=> #<Dog id: 1, name: "ハスキー", created_at: "2019-07-06 23:59:43", updated_at: "2019-07-06 23:59:43">

セレクトボックスのグループ化

ポリモーフィックのデータ構造のセレクトボックスのグループ化(grouped_collection_select)に利用してみた

アウトプット(html)

スクリーンショット 2019-07-07 9.47.37.png

<div class="field">
    <select name="animal[global_target_id]" id="animal_global_target_id">
        <optgroup label="Dog">
            <option value="gid://global-id/Dog/1">ハスキー</option>
            <option value="gid://global-id/Dog/2">ゴールデンレトリバー</option>
            <option value="gid://global-id/Dog/3">コーギー</option>
        </optgroup>
        <optgroup label="Cat">
            <option value="gid://global-id/Cat/1">雑種</option>
            <option value="gid://global-id/Cat/2">スコティッシュ・フォールド</option>
            <option value="gid://global-id/Cat/3">アメリカン・ショートヘア</option>
        </optgroup>
    </select>
</div>

実装

サンプル実装

  • model
class Animal < ApplicationRecord
  belongs_to :target, polymorphic: true

  def global_target_id
    self.target.to_global_id.to_s if target.present?
  end

end
class Dog < ApplicationRecord
  has_one :animal, as: :target

  def global_id
    self.to_global_id.to_s
  end

  class << self
    def human_model_name
      model_name.human
    end
  end

end
class Cat < ApplicationRecord
  has_one :animal, as: :target

  def global_id
    self.to_global_id.to_s
  end

  class << self
    def human_model_name
      model_name.human
    end
  end

end
  • view
<%= form_with(model: animal, local: true) do |form| %>

  <div class="field">
    <%= form.grouped_collection_select :global_target_id, [Dog, Cat], :all, :human_model_name, :global_id, :name, {}, {} %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
  • controller
# POST /animals
# POST /animals.json
def create
  @animal        = Animal.new
  @animal.target = GlobalID::Locator.locate animal_params[:global_target_id]

  respond_to do |format|
    if @animal.save
      format.html { redirect_to @animal, notice: 'Animal was successfully created.' }
      format.json { render :show, status: :created, location: @animal }
    else
      format.html { render :new }
      format.json { render json: @animal.errors, status: :unprocessable_entity }
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails自分用メモ

Rails基本コマンド+基本操作

Railsで新しくアプリのフォルダを立ち上げ

rails new hello_world_sample

下記のようにフォルダやファイルが自動で生成される。
スクリーンショット 2019-07-07 9.54.33.png

データベースを作成する

データベースは、Webアプリケーションの中で使用されるデータを整理整頓し、管理するためのもの。
立ち上げたアプリに関連するデータベースを作成する場合は、以下のコマンドを実行するだけでOK。

bundle exec rake db:create

実行結果(開発環境とテスト環境のデータベースが作成される)↓

Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'

スクリーンショット 2019-07-07 10.02.31.png

ルートの定義

config/routes.rbを開く

ファイルが確認できたら、その中にルートを設定する。
ルートを設定する際は、アクセスされるURL、そのURLに対応するコントローラ、指定したコントローラのアクションを記述。
以下は、ルートを設定するときの一つの例。

routes.rb
# 左側がURL 右側がコントローラの名前とアクション
get '/homes', to: 'homes#index'

上記は以下のような構成と覚えておく。

HTTPメソッド 'ユーザーが指定するURL', to: 'コントローラー名#アクション名'

コントローラの作成

今回は試しに「homes」というコントローラーを作成してみる↓
「homes」←コントローラーは末尾に「s」をつけること(Railsでは複数形が推奨されている)

rails g controller homes

実行結果↓

Running via Spring preloader in process 29505
      create  app/controllers/homes_controller.rb
      invoke  erb
      create    app/views/homes
      invoke  test_unit
      create    test/controllers/homes_controller_test.rb
      invoke  helper
      create    app/helpers/homes_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/homes.coffee
      invoke    scss
      create      app/assets/stylesheets/homes.scss
  • app/controllers/homes_controller.rb
  • app/views/homes
  • test/controllers/homes_controller_test.rb
  • app/helpers/homes_helper.rb
  • app/assets/javascripts/homes.coffee
  • app/assets/stylesheets/homes.scss

↑これが自動で作られたファイル(多い)

アクションを追加

今回は下記のルートを設定したい

routes.rb
# 左側がURL 右側がコントローラの名前とアクション
get '/homes', to: 'homes#index'

なのでhomes_controller.rbに以下のようにアクションを追加する

homes_controller.rb
class HomesController < ApplicationController

    #indexアクションを定義
    def index

    end

end

ビューの作成

ビューファイルは、コントローラのアクションに対応する名前にする。
今回のサンプルであれば、homesコントローラのindexアクションに対応するビューファイルを作成するので、
app/views/homesの中にindex.html.erbというファイルを作成。
スクリーンショット 2019-07-07 10.44.07.png

コントローラからのデータの受け渡し

「コントローラからビューにデータを渡す」ということをやってみる。
下記のようにhomes_controller.rbにインスタンス変数を定義

homes_controller.rb
class HomesController < ApplicationController

    #indexアクションを定義
    def index
        # インスタンス変数を定義
        @greet = "Hello World!!"
    end

end

次にindex.html.erbを下記のように記述する

index.html.erb
<%= @greet %>

ブラウザでhttp://localhost:3000/homesにアクセスすると・・・
スクリーンショット 2019-07-07 10.56.18.png
表示されたー!!

モデルからコントローラへのデータの受け渡し

まずは、/app/modelsuser.rbモデルを作成
スクリーンショット 2019-07-07 11.05.56.png
user.rbは下記のように記述する

user.rb
class User
  def initialize
    @first_name = "kotonoha"
    @last_name = "tukinasi"
    @birthday = "1994/6/7"
    @age = 25
    @birthplace = "Okinawa"
    @hobby = "Twitter"
  end

  def introduce
    <<~EOS

    私の名前は#{@first_name + @last_name}です。
    誕生日は#{@birthday}で、年齢は#{@age}歳。
    出身地は#{@birthplace}で、趣味は#{@hobby}です。

    EOS
  end
end

Userモデルの定義が終わったら、そちらをhomesコントローラからUserモデルをインスタンス化し、
introduceメソッドを実行してその結果を@my_introduceに格納する

homes_controller.rb
class HomesController < ApplicationController

    #indexアクションを定義
    def index
        # インスタンス変数を定義
        @greet = "Hello World!!"

        # Userモデルをインスタンス化
        # 「Userクラス」から「userインスタンス」を作ったと考えると分かりやすい
        user = User.new

        # introduceメソッドを実行
        # 「user.rb」で定義した「introduce」メソッドを使うことができる
        @my_introduce = user.introduce
    end

end

@my_introduceに格納したデータをビューで表示する。
/views/homes/index.html.erbのを以下のように編集。

/views/homes/index.html.erb
<%= @greet %>
<!-- @my_introduceのデータを表示 -->
<%= simple_format(@my_introduce) %>

simple_formatはヘルパーメソッドと呼ばれるものの1つで、
\nや\r\nなどの改行コードを
という改行をさせるためのHTMLタグに変換してくれます。
その結果、ヒアドキュメントの改行がきちんと反映された状態になる。

ブラウザでアクセスすると下記のようになる↓
スクリーンショット 2019-07-07 11.17.43.png

simple_formatを使わない場合

/views/homes/index.html.erb
<%= @greet %>
<!-- @my_introduceのデータを表示 -->
<%= @my_introduce %>

スクリーンショット 2019-07-07 11.19.39.png

改行が無しで、1行で表示されてしまう。

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

Rails基本コマンド+基本操作

Railsで新しくアプリのフォルダを立ち上げ

rails new hello_world_sample

下記のようにフォルダやファイルが自動で生成される。
スクリーンショット 2019-07-07 9.54.33.png

データベースを作成する

データベースは、Webアプリケーションの中で使用されるデータを整理整頓し、管理するためのもの。
立ち上げたアプリに関連するデータベースを作成する場合は、以下のコマンドを実行するだけでOK。

bundle exec rake db:create

実行結果(開発環境とテスト環境のデータベースが作成される)↓

Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'

スクリーンショット 2019-07-07 10.02.31.png

ルートの定義

config/routes.rbを開く

ファイルが確認できたら、その中にルートを設定する。
ルートを設定する際は、アクセスされるURL、そのURLに対応するコントローラ、指定したコントローラのアクションを記述。
以下は、ルートを設定するときの一つの例。

routes.rb
# 左側がURL 右側がコントローラの名前とアクション
get '/homes', to: 'homes#index'

上記は以下のような構成と覚えておく。

HTTPメソッド 'ユーザーが指定するURL', to: 'コントローラー名#アクション名'

コントローラの作成

今回は試しに「homes」というコントローラーを作成してみる↓
「homes」←コントローラーは末尾に「s」をつけること(Railsでは複数形が推奨されている)

rails g controller homes

実行結果↓

Running via Spring preloader in process 29505
      create  app/controllers/homes_controller.rb
      invoke  erb
      create    app/views/homes
      invoke  test_unit
      create    test/controllers/homes_controller_test.rb
      invoke  helper
      create    app/helpers/homes_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/homes.coffee
      invoke    scss
      create      app/assets/stylesheets/homes.scss
  • app/controllers/homes_controller.rb
  • app/views/homes
  • test/controllers/homes_controller_test.rb
  • app/helpers/homes_helper.rb
  • app/assets/javascripts/homes.coffee
  • app/assets/stylesheets/homes.scss

↑これが自動で作られたファイル(多い)

アクションを追加

今回は下記のルートを設定したい

routes.rb
# 左側がURL 右側がコントローラの名前とアクション
get '/homes', to: 'homes#index'

なのでhomes_controller.rbに以下のようにアクションを追加する

homes_controller.rb
class HomesController < ApplicationController

    #indexアクションを定義
    def index

    end

end

ビューの作成

ビューファイルは、コントローラのアクションに対応する名前にする。
今回のサンプルであれば、homesコントローラのindexアクションに対応するビューファイルを作成するので、
app/views/homesの中にindex.html.erbというファイルを作成。
スクリーンショット 2019-07-07 10.44.07.png

コントローラからのデータの受け渡し

「コントローラからビューにデータを渡す」ということをやってみる。
下記のようにhomes_controller.rbにインスタンス変数を定義

homes_controller.rb
class HomesController < ApplicationController

    #indexアクションを定義
    def index
        # インスタンス変数を定義
        @greet = "Hello World!!"
    end

end

次にindex.html.erbを下記のように記述する

index.html.erb
<%= @greet %>

ブラウザでhttp://localhost:3000/homesにアクセスすると・・・
スクリーンショット 2019-07-07 10.56.18.png
表示されたー!!

モデルからコントローラへのデータの受け渡し

まずは、/app/modelsuser.rbモデルを作成
スクリーンショット 2019-07-07 11.05.56.png
user.rbは下記のように記述する

user.rb
class User
  def initialize
    @first_name = "kotonoha"
    @last_name = "tukinasi"
    @birthday = "1994/6/7"
    @age = 25
    @birthplace = "Okinawa"
    @hobby = "Twitter"
  end

  def introduce
    <<~EOS

    私の名前は#{@first_name + @last_name}です。
    誕生日は#{@birthday}で、年齢は#{@age}歳。
    出身地は#{@birthplace}で、趣味は#{@hobby}です。

    EOS
  end
end

Userモデルの定義が終わったら、そちらをhomesコントローラからUserモデルをインスタンス化し、
introduceメソッドを実行してその結果を@my_introduceに格納する

homes_controller.rb
class HomesController < ApplicationController

    #indexアクションを定義
    def index
        # インスタンス変数を定義
        @greet = "Hello World!!"

        # Userモデルをインスタンス化
        # 「Userクラス」から「userインスタンス」を作ったと考えると分かりやすい
        user = User.new

        # introduceメソッドを実行
        # 「user.rb」で定義した「introduce」メソッドを使うことができる
        @my_introduce = user.introduce
    end

end

@my_introduceに格納したデータをビューで表示する。
/views/homes/index.html.erbのを以下のように編集。

/views/homes/index.html.erb
<%= @greet %>
<!-- @my_introduceのデータを表示 -->
<%= simple_format(@my_introduce) %>

simple_formatはヘルパーメソッドと呼ばれるものの1つで、
\nや\r\nなどの改行コードを
という改行をさせるためのHTMLタグに変換してくれます。
その結果、ヒアドキュメントの改行がきちんと反映された状態になる。

ブラウザでアクセスすると下記のようになる↓
スクリーンショット 2019-07-07 11.17.43.png

simple_formatを使わない場合

/views/homes/index.html.erb
<%= @greet %>
<!-- @my_introduceのデータを表示 -->
<%= @my_introduce %>

スクリーンショット 2019-07-07 11.19.39.png

改行が無しで、1行で表示されてしまう。

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

先ほどは手動で/app/modelsuser.rbモデルを作ったが、ターミナルのコマンドでも作成することできる。

rails g model user

実行結果↓

Running via Spring preloader in process 20380
      invoke  active_record
      create    db/migrate/20190707033838_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
  • db/migrate/20190707033838_create_users.rb
  • app/models/user.rb
  • test/models/user_test.rb
  • test/fixtures/users.yml

↑これが自動で作られたファイル

rails g modelコマンドを実行すると、同時にマイグレーションファイルも作成される。
このファイルは、データベースにどのような構造のテーブルを作成するかを指定することができる。
今回は、ユーザーの名前と年齢を登録できるテーブルを作成したいので、
nameとageのカラムが作成されるよう以下のコードを書く。

20190707033838_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
        create_table :users do |t|
            t.string :name
            t.integer :age
            t.timestamps
        end
    end
end

マイグレーションファイルで指定できる型

データ方 説明
string 文字列
text 長い文字列
integer 整数
float 浮動小数
decimal 精度の高い小数
datetime 日時
timestamp より細かい日時
time 時間
date 日付
binary バイナリデータ
boolean Boolean型

マイグレーションファイルの内容をデータベースに反映

bundle exec rake db:migrate

そうすると、アプリに紐づいたデータベースの中に、usersテーブルが作成される。
スクリーンショット 2019-07-07 12.57.27.png
↑どういうテーブルが作成された見たい場合は、db/development.sqlite3を「DB Browser for SQlite」で開くことで見ることができる。
テーブルが作成できたら、次にデータベースにデータを登録するためのルーティングやコントローラの処理を実装していく。

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

Railsで指定したディレクトリ以下のファイルを取得する時の解決方法

Railsで指定したディレクトリ以下のファイルを取得する時につまずいたので共有します。

環境

バージョン

$ rails -v
Rails 5.2.3
$ ruby -v
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]

ディレクトリ構成(該当部分のみ)

app/commands/command.rb
app/commands/command/hoge_command.rb
app/commands/command/fuga_command.rb
app/commands/command/piyo_command.rb

ソースコード

# app/commands/command.rb
class Command
  def initialize(command_name)
    files_abs_pass = Dir[File.expand_path("#{Rails.root}/app/commands/command/", __FILE__) << '/*.rb']

    files_abs_pass.each { |f| puts f }
    # /Users/user/work/rails/rails_app/app/commands/command/hoge_command.rb
    # /Users/user/work/rails/rails_app/app/commands/command/fuga_command.rb
    # /Users/user/work/rails/rails_app/app/commands/command/piyo_command.rb
  end
end
# app/commands/command/hoge_command.rb
class HogeCommand < Command
end

当初はこのページを参考にしていたのですが、ファイル一覧を取得できなかったので変えました。

# 掲載元のコード
Dir[File.expand_path('../commands', __FILE__) << '/*.rb'].each do |file|
  require file
end

JavaのJSPでも同じような事象に悩まされていたので、解決方法はこれでいいんじゃないかって思えましたw(uriの指定)

もっとスマートに書けるはずなので、気が向いたらあとがきにでも書きます。。。(コメント頂けたらとても嬉しいです...!)

引用元

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