- 投稿日:2019-07-07T23:47:46+09:00
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との格闘はまだまだ続きそうです。
- 投稿日:2019-07-07T21:14:42+09:00
Vue.js チュートリアル for Rails エンジニア
はじめに
この記事は、普段 rails を使用して開発を行なっているエンジニアが、 Vue.js を触り始めようとする時に見たら役にたつかもしれないものです。
チュートリアルの内容
チュートリアルは、以下の2本立てで行うことで、 Vue.js に触れたことがない Rails エンジニアでも理解しやすい形にしています。すでに Vue.js に触れたことがある場合は、1を飛ばして、2から始めるのが良いかと思います。
- 簡単な Vue.js アプリケーションの開発
- 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.0Vue.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 environmentwebpacker のインストールが完了すると、普段の 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 shttp://localhost:3000/hello_vue/index にアクセスして、下記のページが表示されると ok です。
javascript_pack_tag の設定
javascript_pack_tagでapp/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.jsimport 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 にアクセスしてみると、下記のような画面になっているかと思います。
環境構築で、
Vue.js devtoolsのインストールが完了している場合、 chrome の拡張機能の部分に、Vのマークが色付きで出てきているはずです。
この時点で、一番の基本となる rails + Vue.js のアプリケーション実装が完了しました。
webpack の自動ビルド
先ほどは、
bin/webpackのビルドを行いましたが、これでは js ファイルを変更するたびに再度コマンドを叩いてビルドをする必要があります。
流石にそれは面倒なので、開発中はbin/webpackの代わりに下記コマンドを実行して、ファイルを保存するたびに自動ビルドが走るようにしておくといいでしょう。[rails-vue-app] $ bin/webpack-dev-serverVue.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メソッドのインスタンス変数として、titledescriptionそして Hash 形式のcontentsを用意します。app/controllers/home_controller.rbclass 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 %>※
data属性に設定した情報は、 developer tool などを使うことで閲覧することができるので、 API 同様にユーザーのプライペート情報など、秘匿情報は公開しないようにして注意してください。表示するコンポーネントの実装
先ほど view で設定したデータを、 js 側から読み取ってみましょう。
app/javascript/packs/home/index.jsを下記の通り実装します。app/javascript/packs/home/index.jsimport 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 を開けてみると、
このように、 rails の view ファイルで設定したデータが、JS の Object 形式で取得できていることがわかります。
この Object データを使用して、実装を進めていきましょう。
home/index.jsでは、home/Indexというコンポーネントを render することを想定して実装を進めていきます。 render 関数の第二引数に、先ほど取得した Object のデータを設定します。app/javascript/packs/home/index.jsimport 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-serverhttp://localhost:3000/home/index にアクセスした時に、下記のようなページが表示されていると成功です。
実装のリファクタリング
とりあえず表示させることを優先で、
Index.vueに全てを書いていたので、簡単にコンポーネントの分割をしていきます。ざっとこんな感じのイメージで分割していきます。
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.jsimport 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.rbclass 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 にアクセスしてみると、下記のような画面が表示されるはずです。
一旦これで、 API のエンドポイントが完成しました。
コンポーネントから API を叩いてデータを取得
コンポーネントと API の Ajax 通信は axios というライブラリを使用して行います。4
まずはaxiosをインストールします。[rails-vue-app] yarn add --dev axiosinstall に成功していると、
package.jsonaxiosの項目が追記されているはずです。package.json"devDependencies": { + "axios": "^0.19.0", "webpack-dev-server": "^3.7.2" }次に
integerLiteralDescriptions/Index.vueを書き換えて、 axios でのデータ取得を実装します。app/javascript/components/integerLiteralDescriptions/Index.vueimport 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 を見てみましょう。
↓のようなログが出て、データ取得ができているはずです。
Tips: JS のデバッグ
ご存知な方も多いと思いますが、 JS では debugger を仕込むことで、デバッグ実行が可能になります。
debugger ステートメント | MDN具体的には下記のように仕込むことができます。
app/javascript/components/integerLiteralDescriptions/Index.vueAxios.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 にアクセスしてみると、
debuggerを仕込んだ部分で処理が止まり、Consoleからその時点の各種データを覗くことができます。(↑画像の場合、画像下部でresponseDataの値を確認しています。)取得したデータをコンポーネント内で描画
先ほど
axiosで取得したデータを、画面に反映させます。
今回の場合、下記の部分を書き換えるだけです。app/javascript/components/integerLiteralDescriptions/Index.vueAxios.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.rburl: '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 に投稿してみることにしました。どこかのエンジニアの役に立つと幸いです。
今回の実装では CoffeeScript と turbolinks のセットアップを省いています。既存の rails プロジェクトで Vue.js を使用する場合は、 turbolinks と戦う必要がありそうです。 ↩
もし既存の rails プロジェクトに途中から導入する場合は、右記をご参考ください。rails/webpacker#installation ↩
- 投稿日:2019-07-07T20:16:57+09:00
HTMLでテーブルのヘッダー(上部と左部)を固定してスクロールさせる実装
はじめに
テーブルのヘッダー(上部と左部)を固定してスクロールさせる実装でプラグインなどを比較したため、備忘録を含めまとめたいと思います。
position:stickの実装方法
まず、大前提として本来であれば
position:stickyを使用することが望ましいかと思いました。
こちらの方の記事が大変わかりやすいです。
https://qiita.com/s0tter/items/14fb4ec2600828a21a22
CSSのみで完結するため上記手法が望ましいかと考えておりますが、2019年7月7日時点でIEに対応しておりません。IEのユーザーもアプリを活用されることが予想されるため、採用を見送りさせていただきました。
https://caniuse.com/#search=position%3A%20stickyposition:sticky対応状況
可能であれば、
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での動作状況
・fixedTblHdrLftCol
Github:https://github.com/nkmrshn/fixedTblHdrLftCol
http://nkmrshn.com/fixedTblHdrLftCol/samples/sample_3_sync.html
最終更新:2014年6月13日IEでの動作状況
・FixwdMidashi
http://hp.vector.co.jp/authors/VA056612/fixed_midashi/manual/index.html
最終更新:2018年12月3日IEでの動作状況
・Grid
(まだまとめている最中です。)
IEで最も綺麗に動作しているのが、FixwdMidashiだったように思えます。
他に良い方法などございましたら、一声かけていただけますと幸いです。少しずつ追加させていただきます。
ご参考になれば幸いです。
- 投稿日:2019-07-07T19:11:48+09:00
【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_databaseのValueが
ローカル環境では、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.ymldefault: &default adapter: mysql2 # ↓これ charset: utf8 encoding: utf8 pool: 5 username: root password: socket: /tmp/mysql.sock4. あとがき
調べてみると、databaseを
dropすることなく文字コードの変更も行えるみたいです。
重要なレコードがある場合は、そちらをご活用ください。
- 投稿日:2019-07-07T18:54:10+09:00
Don't know how to build task 'YOUR CREATE TASK'
ファイル拡張子を
rakeにしましょう!rbにしてた
- 投稿日:2019-07-07T18:35:59+09:00
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 endshowと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が一意のデータを抽出しようとしてるからエラーになったんだと思う
- 投稿日:2019-07-07T16:56:17+09:00
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デプロイ出来た!!(解決)
- 投稿日:2019-07-07T16:46:23+09:00
Herokuへデプロイした時に「The page you were looking for doesn't exist. 」エラーが出る
事象
Railsでアプリを作って、動作確認も問題なし!
「さぁHerokuにデプロイ!」と思い、「git push heroku master」を実施。デプロイされたアプリをherokuのURLを開くと下記のエラーが出た・・・
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.rbRails.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-07T16:41:35+09:00
Ruby on RailsでHelloWorld
はじめに
RailsでHelloWorldする。
環境
MacOS : Mojave
Ruby : 2.3.7
Rails : 5.2.3アプリ作成
rails new helloworld cd helloworldサーバ起動
rails server起動確認
ブラウザからいかにアクセス。
http://localhost:3000/
- 投稿日:2019-07-07T15:24:47+09:00
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 endcontroller がとてもシンプルで読みやすいですね。
複雑な 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 で解決できます。
- 投稿日:2019-07-07T14:34:55+09:00
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します。gemfilegem "devise" gem 'omniauth' gem 'omniauth-facebook' gem 'omniauth-twitter'deviseのインストール
$ rails g devise:installcreate 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:migrateTwitter 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.rbDevise.setup do |config| config.omniauth :facebook, 'App ID', 'App Secret' config.omniauth :twitter, 'API key', 'API secret' end私の場合ローカルで繋がったのが確認出来るまでは直接書いちゃってます。
注意としてここに書いたままで、 githubなどのリモートリポジトリーにあげないこと。
上げるときは、dotenv、figaro、credentials.yml.encなどで管理してください。userモデルにメソッドを追加
models/user.rbclass 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このメソッドは
provideranduidフィールドで既存のユーザーを見つけようとします。ユーザーが見つからない場合は、ランダムなパスワードといくつかの追加情報を使用して新しいユーザーが作成されます。 後ろにくっついている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.rbRails.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.rbclass 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.rbclass 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 %> <!-- -------------ここまで----------------------------------------------------- -->適当で申し訳ないですが、こんな感じでリンクが出ます。
ここまで実装しますと、twitterとfacebookでログインが出来るようになります。編集ページについて
deviseはデフォルトで編集時にパスワードが必要になります。
twitter、facebookでログインするとこの場合はパスワードをランダムで生成してログインしているため、パスワードを把握しておらず、編集が出来ないと思います。そこで編集時にパスワードを入力をしないでいいように編集します。(ただし、パスワードの変更はそのページでは出来なくなります)
私もなんでこうなるのかとか曖昧ですのでこちらを読んでもらえると助かります。
参考リンク↓
github-devise-wiki How To: Allow users to edit their account without providing a passwordルートの作成
config/routes.rbRails.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.rbclass 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:editEDITORを指定してください。今回は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.rbDevise.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" の脆弱性対策-Qiitagemの追加
gemfilegem "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:resetprodction(heroku)
$ heroku pg:reset DATABASEこのコマンドだともう一度
heroku run rails db:migrateを行う必要があります。
多分もっといいコマンドがあるはず。最後に
これでtwitterとfacebookでログインすることができます。
まだまだ、書いている途中なので、付け加えて行きます。
- 投稿日:2019-07-07T14:28:50+09:00
配列
自分用忘備録
def new
@category_parent_array = ["---"]
Category.where(ancestry: nil).each do |parent|
@category_parent_array << parent.name
end
end@category_parent_array = ["---"]
[]の中に初期値を設定している。"---"が0番となり、こんな感じで最初に表示される。
@category_parent_array << parent.nameでその後追加されるもの1番以下で続く。
- 投稿日:2019-07-07T13:24:38+09:00
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: trueenum 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
- 投稿日:2019-07-07T12:50:57+09:00
リクエストのスタブ化ができる 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 をインストールします。Gemfilegem '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 リクエストもスタブ化できるようなので、利用の幅が広いなと感じました。
これから試してみようと思います。
参考
- 投稿日:2019-07-07T12:27:42+09:00
たのしい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#cookiesRails側から処理を追います?
まずはActionController::Cookies#cookiesメソッドを読んでみましょう。ActionController::Cookies#cookies
actionpack/lib/action_controller/metal/cookies.rb11 private 12 def cookies 13 request.cookie_jar 14 end一番最初に呼ばれる
#cookiesメソッドの中身はこんな感じ、レシーバに対して#cookie_jarメソッドを呼んでいるだけ。レシーバの
requestはActionDispatch::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.rb11 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_headerRack側に移ります?
Rack::Request::Env#fetch_header
rack/lib/rack/request.rb69 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.rb12 fetch_header("action_dispatch.cookies") do 13 self.cookie_jar = Cookies::CookieJar.build(self, cookies) 14 end&blockとは
引数の
&blockはfetch_headerに渡されているブロック
self.cookie_jar = Cookies::CookieJar.build(self, cookies)。(参考)
actionpack/lib/action_dispatch/middleware/cookies.rb12 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.rb28 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.rb79 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.rb11 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)が、その前に
注目
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.rb215 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.rb216 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.rb79 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.rb219 string = get_header HTTP_COOKIE
HTTP_COOKIEを引数として、get_headerメソッドを呼んでいます。
get_headerはRack::Request::Env#get_headerで、定義は以下の通り。lib/rack/request.rb63 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に{}stringにget_header HTTP_COOKIEの返り値(初めてCookieを使用する場合はnil)が代入されていることになります。
lib/rack/request.rb221 return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
RACK_REQUEST_COOKIE_STRINGを引数に、Rack::Request::Env#get_headerメソッドを呼んでstringと比較。(参考)
lib/rack/request.rb63 def get_header(name) 64 @env[name] 65 endRACK_REQUEST_COOKIE_STRINGとは?
ここで登場する
RACK_REQUEST_COOKIE_STRINGの正体はrack.request.cookie_stringで、こちらもサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)example.rb_todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06(御多分に洩れず↑の例も先ほどの記事より)
…
HTTP_COOKIEと同じですね!つづき
lib/rack/request.rb221 return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
==の場合はここでreturn hashなぜ?
次行以降はリクエストヘッダからやって来た
hash(初めてのCookieを使用する場合は空の{})をHTTP_COOKIEで更新する処理となっています。
ハッシュの中身が変更されていない場合、置き換える必要がないため。初めてCookieを使用する場合、
stringとget_header(RACK_REQUEST_COOKIE_STRING)の返り値はいずれもnilのため、ここでcookiesメソッドの処理はhash(空の{})を返して終了します。つづき(すでにCookieが存在する場合のみ)
lib/rack/request.rb222 hash.replace Utils.parse_cookies_header stringここでは、
Utils.parse_cookies_header(string)の返り値{ Cookieの名前: Cookieの値 }で、hash(fetch_header(RACK_REQUEST_COOKIE_HASH)の返り値)をreplaceしています。
Utils.parse_cookies_headerはRack::Utilsのメソッドで、
詳しくは読みませんが、
string(Cookieの名前=Cookieの値)を{Cookieの名前: Cookieの値}
のようなハッシュに変換する役割を担っています。(参考)
lib/rack/utils.rb209 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.rb223 set_header(RACK_REQUEST_COOKIE_STRING, string) 224 hash 225 endすなわち、ここで
@env[RACK_REQUEST_COOKIE_STRING] = stringという処理を実行。
(参考)
lib/rack/request.rb79 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.rb170 # 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は一番最初のrequest(ActionDispatch::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.rb170 # 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.rb295 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.rb170 # 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.rb334 def update(other_hash) 335 @cookies.update other_hash.stringify_keys 336 self 337 end引数other_hashとは
引数
other_hashは先ほどのcookies、つまり{Cookieの名前: Cookieの値}もしくは空の{}。
@cookiesとは
@cookiesはinitializeの中で初期化したこの部分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.rb13 self.cookie_jar = Cookies::CookieJar.build(self, cookies)ここで
self.cookie_jar=メソッドに引数として渡されて、actionpack/lib/action_dispatch/middleware/cookies.rb28 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.rb170 # 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.rb374 def []=(name, options)これを定義すると、
[:name] = optionsという形で値を代入できるようになります。
それではここで、Railsアプリからcookies[]= に渡せる値を確認しましょう?
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.rb375 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.rb378 else 379 value = options 380 options = { value: value } 381 end
cookies[:hoge] = "fuga"の場合、変数valueに"fuga"を代入。
optionsに{value: "fuga"}を再代入。今の状態
①②いずれの場合も、
optionsは{value: "fuga"}valueはfugaとなっている。
つづき
actionpack/lib/action_dispatch/middleware/cookies.rb383 handle_options(options)同じクラスの
handle_options(options)を呼びます。ActionDispatch::Cookies::CookieJar#handle_options
handle_optionsはoptionsをいい感じにするためのメソッドです。actionpack/lib/action_dispatch/middleware/cookies.rb350 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.rb351 if options[:expires].respond_to?(:from_now) 352 options[:expires] = options[:expires].from_now 353 end
cookiesメソッドに有効期限を設定した場合(例:1.week)、from_nowで具体的な日時に変換してoptions[:expires]に再代入。
from_nowはsinceのエイリアス。actionpack/lib/action_dispatch/middleware/cookies.rb355 options[:path] ||= "/"
cookiesメソッドにパスが指定されていない場合、ルートパスを設定(デフォルト値として)actionpack/lib/action_dispatch/middleware/cookies.rb357 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'}
-valueはfugaとなっています。
ActionDispatch::Cookies::CookieJar#[]=のつづき
actionpack/lib/action_dispatch/middleware/cookies.rb385 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::Cookiesはcallメソッドを持った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.rb170 # 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.rb647 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#write→Cookies::CookieJar#make_set_cookie_headerから、最終的に呼ばれる::Rack::Utils.add_cookie_to_header(↑の資料では未出)
- ヘッダーに書き込む文字列の成形を行なっていることが確認できるが、その後実際にヘッダーに書き込まれるタイミング
おつかれさまでした?
参考資料
- Ruby on Railsチュートリアル 第9章 発展的なログイン機構 9.1.2 ログイン状態の保持
- RFC 6265 HTTP 状態管理の仕組み( “HTTP cookie” )
- SuikaWiki メタ変数 - プロトコル依存のメタ変数
- Rack: a Ruby Webserver Interface
- TraceLocation
おまけ
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
- 投稿日:2019-07-07T10:59:41+09:00
データベースに保存されている時刻とビューに表示される時刻が違う
問題:データベースの値とビューに表示される時刻が違う
仮説
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, DateTime2.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行目)
でも引っ張り出すと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月からエンジニアになりました
間違っているところがありましたらご指摘ください
時間ある時に修正します
何卒よろしくお願いします
- 投稿日:2019-07-07T10:26:00+09:00
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ができることです。
図にすると以下のような感じです。Table of contents
- Dockerコンテナの準備
- Nuxt.jsのHello world
- Rails(API)のHello world
- 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.ymlfront/DockerfileFROM 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 8080back/DockerfileFROM 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/Gemfilesource 'https://rubygems.org' gem 'rails', '~>5'docker-compose.ymlversion: "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 build2. 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完了です!(参考)Nuxt.jsではホットリローディングというファイルの変更を自動で反映してくれる機能を有効にすることができます。
npm run devコマンドでホットリローディングが有効になると公式で説明されていますが何やらうまくいくときといかない時がありました...うまくいかない場合は「IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話 - エンジニアの卵の成長日記」を参考に以下のような設定を書き加えることでホットリロードされるようになりました。front/nuxt.config.jsexport 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 --apiRailsアプリが作成されたら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:createDBの作成が完了したら、scaffoldでAPIを作ってみます。
name属性をもつUserモデルを作ります。$ docker-compose run --rm back rails g scaffold user name:string $ docker-compose run --rm back rails db:migrateHello 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 down4. 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でやりたいことは、
http://localhost:8080/users/:idにアクセスして「Hello, (User.name)」と表示させたい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.vuefront/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 up4-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.jsexport default function({ $axios, redirect }) { $axios.setToken('access_token') $axios.onResponse(config => { $axios.setHeader('Access-Control-Allow-Origin', 'http://back:3000') }) }front/nuxt.config.jsexport 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)にアクセスすると下のような画面が出てきます。
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
- Nuxt.js - ユニバーサル Vue.js アプリケーション
- docker で nuxt.js を開発環境を建てるだけ - Qiita
- Rails on Docker(alpine)でAPIコンテナをつくってみた - Qiita
- IT研修でVuePress+Express+Nuxt on Dockerでシステムを作成した話 - エンジニアの卵の成長日記
- Axios モジュールの使いかた | Nuxt.js における REST API の活用
- Nuxt.jsのメソッド内で外部APIを叩くとcorsエラーが起きる - Qiita
- nuxt.js で axios から外部APIを叩くとCORSエラーを解決 - Qiita
- Hot reloading doesn't work in docker container on Windows · Issue #2481 · nuxt/nuxt.js
- プログラムによるナビゲーション | Vue Router
- 投稿日:2019-07-07T10:00:22+09:00
GlobalIDでセレクトボックスのグループ化(polymorphic)
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)
<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 endclass 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 endclass 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
- 投稿日:2019-07-07T09:59:04+09:00
Rails自分用メモ
Rails基本コマンド+基本操作
Railsで新しくアプリのフォルダを立ち上げ
rails new hello_world_sampleデータベースを作成する
データベースは、Webアプリケーションの中で使用されるデータを整理整頓し、管理するためのもの。
立ち上げたアプリに関連するデータベースを作成する場合は、以下のコマンドを実行するだけでOK。bundle exec rake db:create実行結果(開発環境とテスト環境のデータベースが作成される)↓
Created database 'db/development.sqlite3' Created database 'db/test.sqlite3'ルートの定義
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.rbclass HomesController < ApplicationController #indexアクションを定義 def index end endビューの作成
ビューファイルは、コントローラのアクションに対応する名前にする。
今回のサンプルであれば、homesコントローラのindexアクションに対応するビューファイルを作成するので、
app/views/homesの中にindex.html.erbというファイルを作成。
コントローラからのデータの受け渡し
「コントローラからビューにデータを渡す」ということをやってみる。
下記のようにhomes_controller.rbにインスタンス変数を定義homes_controller.rbclass HomesController < ApplicationController #indexアクションを定義 def index # インスタンス変数を定義 @greet = "Hello World!!" end end次に
index.html.erbを下記のように記述するindex.html.erb<%= @greet %>ブラウザでhttp://localhost:3000/homesにアクセスすると・・・
表示されたー!!モデルからコントローラへのデータの受け渡し
まずは、/app/modelsに
user.rbモデルを作成
user.rbは下記のように記述するuser.rbclass 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 endUserモデルの定義が終わったら、そちらをhomesコントローラからUserモデルをインスタンス化し、
introduceメソッドを実行してその結果を@my_introduceに格納するhomes_controller.rbclass 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タグに変換してくれます。
その結果、ヒアドキュメントの改行がきちんと反映された状態になる。simple_formatを使わない場合
/views/homes/index.html.erb<%= @greet %> <!-- @my_introduceのデータを表示 --> <%= @my_introduce %>改行が無しで、1行で表示されてしまう。
- 投稿日:2019-07-07T09:59:04+09:00
Rails基本コマンド+基本操作
Railsで新しくアプリのフォルダを立ち上げ
rails new hello_world_sampleデータベースを作成する
データベースは、Webアプリケーションの中で使用されるデータを整理整頓し、管理するためのもの。
立ち上げたアプリに関連するデータベースを作成する場合は、以下のコマンドを実行するだけでOK。bundle exec rake db:create実行結果(開発環境とテスト環境のデータベースが作成される)↓
Created database 'db/development.sqlite3' Created database 'db/test.sqlite3'ルートの定義
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.rbclass HomesController < ApplicationController #indexアクションを定義 def index end endビューの作成
ビューファイルは、コントローラのアクションに対応する名前にする。
今回のサンプルであれば、homesコントローラのindexアクションに対応するビューファイルを作成するので、
app/views/homesの中にindex.html.erbというファイルを作成。
コントローラからのデータの受け渡し
「コントローラからビューにデータを渡す」ということをやってみる。
下記のようにhomes_controller.rbにインスタンス変数を定義homes_controller.rbclass HomesController < ApplicationController #indexアクションを定義 def index # インスタンス変数を定義 @greet = "Hello World!!" end end次に
index.html.erbを下記のように記述するindex.html.erb<%= @greet %>ブラウザでhttp://localhost:3000/homesにアクセスすると・・・
表示されたー!!モデルからコントローラへのデータの受け渡し
まずは、/app/modelsに
user.rbモデルを作成
user.rbは下記のように記述するuser.rbclass 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 endUserモデルの定義が終わったら、そちらをhomesコントローラからUserモデルをインスタンス化し、
introduceメソッドを実行してその結果を@my_introduceに格納するhomes_controller.rbclass 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タグに変換してくれます。
その結果、ヒアドキュメントの改行がきちんと反映された状態になる。simple_formatを使わない場合
/views/homes/index.html.erb<%= @greet %> <!-- @my_introduceのデータを表示 --> <%= @my_introduce %>改行が無しで、1行で表示されてしまう。
モデルファイルとマイグレーションファイルの作成
先ほどは手動で/app/modelsに
user.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.rbclass 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テーブルが作成される。
↑どういうテーブルが作成された見たい場合は、db/development.sqlite3を「DB Browser for SQlite」で開くことで見ることができる。
テーブルが作成できたら、次にデータベースにデータを登録するためのルーティングやコントローラの処理を実装していく。
- 投稿日:2019-07-07T05:25:54+09:00
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 endJavaのJSPでも同じような事象に悩まされていたので、解決方法はこれでいいんじゃないかって思えましたw(uriの指定)
もっとスマートに書けるはずなので、気が向いたらあとがきにでも書きます。。。(コメント頂けたらとても嬉しいです...!)
引用元








































