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

【Rails】belongs_toで関連付けしたけどデータが保存出来ない →単数形にしてる?

はじめに

Railsアプリ作成中、belongs_toで関連付けたモデルのデータ保存がうまくいかず、はじかれてしまったので解決した方法です。

しょうもないミスでした:sweat_smile:

この記事が役に立つ方

  • belongs_toで関連付けはしたが、うまく保存が出来ない方

この記事のメリット

  • 無事にデータが保存出来るようになる。

環境

  • macOS Catalina 10.15.1
  • zsh: 5.7.1
  • Ruby: 2.6.5
  • Rails: 5.2.3

エラー内容

1対多の関係にある、UserモデルとRecordモデルがあるとします。

Recordテーブルのデータを新規作成し、save!でデータ保存しようとしたところ、以下のようなエラーが発生。

ActiveRecord::RecordInvalid: Validation failed: Users must exist

保存するデータには外部キーuser_idがあり、そこにはちゃんと値が入っていて、「あれ?なんで?」と困る。

このときのモデルは以下のように定義していました。

app/models/record.rb
class Record < ApplicationRecord
  belongs_to :users
end

ここから応急処置(間違った解決法)とちゃんとした解決法を記載します。

1.応急処置(間違った解決法)

以下のように変更。

【Before】

app/models/record.rb
class Record < ApplicationRecord
  belongs_to :users
end


【After】

app/models/record.rb
class Record < ApplicationRecord
  belongs_to :users, optional: true
end

optional: trueで関連モデルなしでOK!と放し飼い状態にし、一旦。バリデーションを回避。

しかし、全く意味のない矛盾した設定になるのであくまでも応急処置。

割とググるとこの解決法が多かったですが、あまり良くないと思います。

2.解決(ただの設定ミス)

【Before】

app/models/record.rb
class Record < ApplicationRecord
  belongs_to :users, optional: true
end


【After】

app/models/record.rb
class Record < ApplicationRecord
  belongs_to :user
end

:x: 複数形
:o: 単数形

belongs_to関連付けで指定するモデル名は必ず「単数形」にしなければなりません。

Rails ガイドにもちゃんと書いてました!

英語で考えたら当たり前、お恥ずかしいです:tired_face:
optional: trueも不要なので削除。

これで問題なし!

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

しょうもない設定ミスで時間をロスしてしまったので、同じようなエラーで困っている方がいれば参考にして頂ければと思います:sweat_smile:

参考にさせて頂いたサイト(いつもありがとうございます)

Active Record の関連付け - Rails ガイド

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

rails-tutorial第4章

helperメソッドとは

便利そうなメソッドをhelperに定義しておいてそれを呼び出す。

app/helpers/application_helper.rb
module ApplicationHelper

  # ページごとの完全なタイトルを返します。
  def full_title(page_title = '')
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end
end

これにより、page_titleが空の場合は、縦棒をいれずにbase_titleだけをタイトルタグに入れることができる。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

titleタグを先ほどhelperに定義したdef full_title(page_title = '')を使って実装している。
yield(:title)によりprovide(:title, "About")のように定義したタイトルを参照できる。
provideとyieldはセットで考える。

pry

irbを拡張したものとしてpryという機能がある。
これはgemをインストールすることで使うことができる。

$ gem intall pry
$ pry

このコマンドにより、pryを使える。

putsメソッドについて

[3] pry(main)> puts 'foo'
foo
=> nil

上記のnilは計算の結果を返している。そのため、putsはfooを出力するがメソッド自体は何も返さない。

?の付くメソッド

?の付くメソッドは基本的にtrue か falseを返すようになっている。
自分でメソッドを定義するときもそれを気をつけながら定義する。

nilと''の違い

nilをto_sメソッドで文字列にしたものが''
つまり、''は0文字の文字列である。

Ruby(オブジェクト指向スクリプト言語)について

Rubyはメソッドを呼び出すという言い回しをしているが、実は少し違う。
オブジェクト指向スクリプト言語は、オブジェクトとのメッセージのやりとりだと考える。

[18] pry(main)> 'foobar'.length
=> 6

上記は、foobarの文字列は何文字?
俺は6文字だよー
という会話で成り立っている。

ブロックの考え方

>> (1..5).each { |i| puts 2 * i }
2
4
6
8
10
=> 1..5

ブロック付きメソッドはメソッドの引数にRubyの処理を渡していると考えるとわかりやすい。

メソッドチェーンについて

[20] pry(main)> %w[foo bar baz].map { |i| i.upcase }.join
=> "FOOBARBAZ"

このようにメソッドで評価された値にさらに . でメッセージを与えることをメソッドチェーンという。

ハッシュでなんでシンボルを使うのか?

[30] pry(main)> 'name'.object_id
=> 23088500
[31] pry(main)> :name.object_id
=> 88028
[32] pry(main)> 'name'.object_id
=> 24142460
[33] pry(main)> :name.object_id
=> 88028

文字列オブジェクトはオブジェクトidが毎回違う。整数はオブジェクトidが毎回同じ。ただ、シンボルにすると、object_idが毎回同じになっている。
labelとしてシンボルオブジェクトを使うと毎回同じなので毎回計算する必要がない。シンボルは人間にもわかりやすい表現ができ、整数のように計算するコスト削減ができパソコンにも優しいということ。

ハッシュの省略記法

[34] pry(main)> h1 = {:first => 'yokota', :second => 'daiki' }
=> {:first=>"yokota", :second=>"daiki"}
[35] pry(main)> h1
=> {:first=>"yokota", :second=>"daiki"}
[36] pry(main)> h2 = { first: 'yokota', second: 'daiki' }
=> {:first=>"yokota", :second=>"daiki"}
[37] pry(main)> h2
=> {:first=>"yokota", :second=>"daiki"}

シンボルを右側に書き、=>を省略した形で書くことができる。

classもオブジェクトの1つ

Rubyではあらゆるものがオブジェクトである。それはclassも例外ではない。
Stringにclassをするとclassクラスということがわかる。
Classクラスにsuperclassをするとmoduleクラスということがわかる。

また自分でクラスを定義するとき、親クラスに何も指定しないと、デフォルトでObjectクラスが継承される。

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

Rails10年選手がGoの勉強会に参戦してきた

はじめに

静岡でRailsのWebエンジニアをしているkazuomatzです。2019年も残り1ヶ月を切りましたが、どうやら最近は業界的にRailsはオワコンらしいです。

僕は、2007年くらいからRailsを使ってきたので、かれこれ10年以上Railsを触ってきたことになるわけですが、オワコンなんて言われると、このままRailsを使い続けるか、それとも新しい言語に乗り換えるかの岐路に立たされているような気にもなってきます。

Railsが世の中に登場したのが2004年、2019年現在、Railsのバージョンは6.0です。2007年の12月にRailsのバージョン2が公開されていますから、かれこれ、5世代のRailsを使ってきたことになります。

Web2.0とRailsとの出会い

ちょっと昔話になりますが、2000年半ばに「Web」はバージョン1からバージョン2にバージョンバップされました。「Web 2.0」です。

これまでのWeb(=Web 1.0)は、ユーザーが情報を取得するだけのものでした。それがWeb 2.0になると、ユーザーがWebを通じて情報を発信することができるようになりました。そして、Webをプラットフォームとした様々なサービスやビジネスが生み出される時代に向かっていきました。

今となっては、Web 2.0とは結局何だったのかという議論もありますが、ともかく2007年11月に「Web 2.0 Expo」が盛大に開催されました。Web 2.0を提唱したティム・オライリーの講演をはじめ、当時の先進的なWeb関連のセッションが盛りだくさんでした。

当時、僕はJavaプログラマでマッシュアップ関連のWebサービスをいろいろ作っていましたが、そのWeb 2.0 Expoの会場で、Sun Microsystems(当時)のティム・ブレイの衝撃的な一言を聞くことになります。

Ruby on Railsに触れたJavaプログラマーは、もうJavaには戻ってこないだろう

Javaの開発元であるSun Microsystemsの中の人がこれ言っちゃったわけですから、大変なことです。少なくともWebが大きく変わるぞって時に、この発言によってRailsは脚光を浴びることになります。恐らく、これきっかけでRails始めた人はかなりいたはずです。

僕もその一人です。あの時からこの10年あまり、Railsに魅了され、RailsでいろいろなWebサービスを開発してきました。

Go勉強会へ

前置きが長くなりました。

ともあれ、Railsオワコン説を聞く中で、10年Railsを触ってきた身として、確かにいろいろ辛いことが多くなってきたなぁとも思います。そんな中見つけたのが、Shizuoka.goの勉強会でした。

勉強会の運営者も以前の仕事仲間(@secondarykey)だったので、Rails10年選手がGoを模索しているというネタで何かしゃべらせてとお願いして、参加することにしました。

go.png

ここからは、発表した内容の抜粋です。

Railsのここがよかった

  • 設定より規約 / DRY ( Don’t Repeat Yourself )の哲学
  • フルスタックなWebフレームワーク MVC / ORM / Asset
  • 便利なライブラリ(Ruby Gems)が豊富!!
  • お手軽な動的型付言語 Ruby
  • 開発効率が半端ない ような気がしてた・・・

とにかくRailsに魅了されていた過去の自分。

Railsがオワコンの原因について

  • フロントエンドのフレームワークがいろいろ出てきた
  • Webpackなどのモジュールバンドラーが便利
  • Railsでイケてたところのほころびが出てきた

僕もVue.jsは結構使うんですが、フロントエンドのフレームワークとRailsとがうまく調和しなくなってきてますね。Webpackについては、Rails向けのWebpackerがありますが、これもいまいち、むしろWebpackをそのまま使う人も多しね。Sprocketsもnode対応したり、ES6対応したりでこれでいけないこともないし・・・。

むしろ、僕が困ったと思っているのは、イケてたところのほころびが出てきたと書いたところ。

RailsってRubyGemsにものすごく多くの便利なライブラリーたちがあって、これをサクッと導入することで、いろいろなことが簡単にできます。もちろん、Ruby/Rails以外の言語やフレームワークでもそのような仕組みはあるわけだけど、Railsはちょっと探すと大抵のやりたいことは、Gem入れれば実現できちゃった。それが、当時のRailsって開発効率いいねという神話を作っていたのかもしれません。

rails.jpg

こんなの見ちゃうと、Railsのレールに乗っているイケてる感に拍車かかりますしね・・・。

ところが、10年もいろいろWebサービスを作ってくると、サービス中止になるものもあれば(まぁこちらの方が多いわけですが)、セキュリティ対応やRailsのバージョンアップをしながら保守し続けるサービスもあります。この後者のサービスの延命が辛いんです。

Railsのバージョンアップは痛みを伴います。むしろ作り替えてしまった方が早い場合もあります。いや、作り替えられればまだいいかもしれません。データベースに貯まった様々なデータが特定のライブラリー依存の形で保存されているとかなり辛いです。

例えば、PaperClipという便利なGemがあります。画像ファイルをアップロードすると、Amazon S3などのクラウドストレージにアップロードしてくれる便利なライブラリです。これ本当によく使いました。でも、このPaperClipは、Rails 5.2以降では使えません。開発者の方が開発中止を宣言して、Rails5.2以降では、Railsに標準装備されたActiveStorageを使ってねということになっています。

これから新規に開発するサービスであれば、Rails 6でActiveStorageを使えばよいですが、すでにPaperClipを使ってデータも保存されているサービスをRails 6に移行することは簡単にはできません。

これは、Railsが悪いわけでも、開発を止めてしまったデベロッパーが悪いわけではありません。むしろ便利なGemを使うことでイケてる感を出してきたツケが回ってきたのかなと思います。

今のRuby / Railsの状況

  • 2〜3年前に開発したWebサービスがサポート外のRailsで動いている
  • とはいえ、Railsのバージョンアップする時間・費用がない
  • 慣れ親しんだライブラリーが更新されず、最新のRails環境で使えない

こんな状況をGoは救ってくれるのか

ここからやっとGoの話。こんな状況をGoは救ってくれるのかを検証してみた。

これからもWebで食べていく僕がやっていくことは、こんなこと。

  • Webサービスの開発(バックエンドもフロントエンドもやる)
  • アプリ開発におけるバックエンドの開発(iOS / Androidアプリも作る)
  • AWSを上手に使ってマイクロサービス指向で行く(Azure、GCPにも手を出す)

願うこと

  • Web開発に必要なよいフレームワーク
  • シンプルに開発ができること
  • リファクタリングしやすく保守性が高いこと

発表しながら気づいた。第一にフレームワークを求めちゃうのは、フルスタックな全部入りのRailsにずいぶん甘やかされてきたんだなぁと。Rubyの世界ではWebフレームワークと言ったらほぼRails一択だけど、Goの場合は、軽量級なものからフルスタックなものまで、いろいろなフレームワークが存在する。Routerはこれで、ORMはこれをといった選択することも可能。Railsやってるとこの感覚は確かにない。

デモアプリをGoで作ったよ

僕は、この発表をするにあたり、簡単なWebサイトのモックをGoで作ってみた。

静岡市が運営している市民協働のポータルサイト「ここからネット」。このサイトには市民活動団体の活動情報や開催する講座やイベント情報、ボランティア情報などが掲載されている。そしてこれらデータはオープンデータとして公開されており、Web APIでデータを取得できる。APIのリファレンスもGithubで公開されている。

ここからネットに登録されているデータの中から静岡市の保育施設の一覧を取得して表示するだけのシンプルなものを作ってみた。

できあがりはこんな感じ。

APIからの戻りはこんな感じのJSON。

search.json
{
  "status": 200,
  "page": 1,
  "per_page": 2,
  "all_page_num": 133,
  "count": 2,
  "all_count": 266,
  "pois": [
    {
      "id": 3450,
      "name": "静岡大学教育学部附属幼稚園",
      "prefecture_name": "静岡県",
      "city_name": "静岡市葵区",
      "address1": "大岩町1-10",
      "address2": "",
      "tel": "054-245-1191",
      "url": "http://fzk.ed.shizuoka.ac.jp/youchien/",
      "option_items": [
        {
          "display_name": "分類",
          "attribute_name": "分類",
          "value": "従来どおりの幼稚園"
        },
        {
          "display_name": "設立",
          "attribute_name": "設立",
          "value": "国立"
        },
        {
          "display_name": "受入",
          "attribute_name": "受入",
          "value": "3歳~就学前"
        },
        {
          "display_name": "情報登録日",
          "attribute_name": "情報登録日",
          "value": "H29.9.1"
        }
      ],
      "updated_at": "2018/05/09 22:01:39 +0900",
      "lat": 34.9913406,
      "lng": 138.3794399
    }
  ]
}

GoでAPIをリクエストしてJSON Parseしてクライアントに返すプログラムはこんな感じ。
動的言語のRubyと違って静的型付言語のGoにおいては、APIで受け取るJSONの構造体をキチンと書く。

/controllers/kindergarten.go
// PoiData 受信データ
type PoiData struct {
    Status     int `json:"status"`
    Page       int `json:"page"`
    PerPage    int `json:"per_page"`
    AllPageNum int `json:"all_page_num"`
    Count      int `json:"count"`
    AllCount   int `json:"all_count"`
    Pois       []struct {
        ID             int    `json:"id"`
        Name           string `json:"name"`
        Kana           string `json:"kana"`
        Description    string `json:"description"`
        ZipCode        string `json:"zip_code"`
        PrefectureName string `json:"prefecture_name"`
        CityName       string `json:"city_name"`
        Address1       string `json:"address1"`
        Address2       string `json:"address2"`
        Tel            string `json:"tel"`
        URL            string `json:"url"`
        ImageURL       string `json:"image_url"`
        OptionItems    []struct {
            DisplayName   string `json:"display_name"`
            AttributeName string `json:"attribute_name"`
            Value         string `json:"value"`
        } `json:"option_items"`
        StartAt        string  `json:"start_at"`
        EndAt          string  `json:"end_at"`
        LocationID     string  `json:"location_id"`
        ActivityID     string  `json:"activity_id"`
        PostPhotoID    string  `json:"post_photo_id"`
        InformationID  string  `json:"information_id"`
        UpdatedAt      string  `json:"updated_at"`
        OrganizationID string  `json:"organization_id"`
        Lat            float64 `json:"lat"`
        Lng            float64 `json:"lng"`
        Marker2X       string  `json:"marker2x"`
        Marker         string  `json:"marker"`
        Type           string  `json:"type"`
    } `json:"pois"`
}

// GetKinderGarten 保育園情報を取得する
func (controller *KinderGartenController) GetKinderGarten() {

    url := beego.AppConfig.String("endPointURL") + "/map/search.json"

    page := controller.GetString("page")
    url += "?page=" + page

    dataSet := controller.GetString("type")
    if len(dataSet) == 0 {
        dataSet = "1,2,3,4,5"
    }
    url += "&data_set=" + dataSet

    response, err := http.Get(url)
    if err != nil {
        controller.CustomAbort(500, "Internal server error")
        return
    }

    defer response.Body.Close()

    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        controller.CustomAbort(500, "Internal server error")
        return
    }

    jsonBytes := ([]byte)(body)
    data := new(PoiData)

    if err := json.Unmarshal(jsonBytes, data); err != nil {
        controller.CustomAbort(500, "Internal server error")
        return
    }
    controller.Data["json"] = data
    controller.ServeJSON()
}

フロントエンドはVue.jsを使っている。モジュールバンドルはWebpackだ。ここはGoとは直接関係ない世界。

選択したWebフレームワーク

そして、肝心のWebフレームワークはいろいろ探した結果、最終的にBeego Frameworkを選んだ。

beego.png

BeegoはRubyのWebフレームワークSinatraを意識して開発されたとのこと。SinatraはRubyのWebフレームワークではRailsの次に使われているもので、フルスタックなRailsよりも軽量なもの。

Beegoは、ORM、Routerも使えて、MVCモデルもいける。CLIもあってコマンド叩いていろいろできる。

実は、自分の発表の前に、元仕事仲間(@secondarykey)がGoのWebフレームワークをいろいろ紹介してくれていた(資料)。彼の所感では、Beegoは「老舗っぽくフルスッタックで色々機能がありそう。重そう。」とのこと。やっぱり、Railsのフルスタック脳がBeegoを選んだ結果は納得できる。

まとめ

Go言語とRubyを比較すると、静的と動的の議論も確かに出てきます。やっぱり静的の方が安全だよねという風潮もありますし、動的の柔軟さが開発効率をあげているのも事実です。

このあたりはトレードオフなので、要件に合わせてどちらが最適かを見極めていくことも必要かなと思います(今まで動的万歳だった自戒も込めて)。

今、自分がサクッとWebサービスを作るとしたら、やはりRailsを選ぶと思います。Rails 6のActiveStorageはよくできていると思いますし、まだまだ使えるGemも多い。自前のライブラリーやTipsも豊富にあったりするので、ユーザビリティや品質の高いWebサービスはまだまだRailsで作れます。

ただ、今回、Shizuoka.goに参加して、Goの魅力にも気づけたことも事実です。
自分が今後やっていきたいことの中で、「AWSを上手に使ってマイクロサービス指向で行く」をあげていますが、こちらはRubyよりもGoが向いているんだろうと思います。AWSでなくて、GCPなのかもしれませんが・・・。

ということで、今までやってきたアーキテクチャーとは違うものにチャレンジする際に、Goを選択することをここに誓います。

今回のShizuoka.goではWebフレームワークの話のほかに、@hogedigoさんのGoでのテスト手法についての話(資料)、@cupperさんのAWSとGoの話(資料)、@hrs_sano645
さんのGo製便利ツールの紹介(資料)など、興味深い話がたくさん聞けました。

最後に、Shizuoka.goのみなさんありがとうございました。また参加します。Shizuoka.go最高!

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

Rubyで生年月日から年齢を求める(ydayメソッド使用)

Dateインスタンスで作成した生年月日から現在の年齢を算出したい時。

誕生日の前か後かも考慮したい場合はydayメソッドで比較的わかりやすく記述できるかと思います。

ydayメソッドは、1月1日を1日目として、オブジェクトの日付が何日目かを整数で返してくれるメソッドです。閏年もカバーしています。

たとえば、2019年2月10日(2019-2-10)なら、返ってくる数値は41となります。

y.dayメソッドを使った年齢算出コード

require "date"

birthday = Date.new(1999, 12, 10)
today = Date.today #=> 2019-11-30と仮定
age = today.year - birthday.year

if today.yday < birthday.yday
  age -= 1
end

puts age #=> 19

上記のコードでは、今日までの日数(today.yday)と誕生日までの日数(birthday.yday)を比較し、前者の方が小さければ誕生日はまだ来ていないのでageからマイナス1しています。

他にも色々な算出方法があるかと思いますが、方法の一つとして参考にしていただければ幸いです。

「このコードだとこんな場合は間違った結果になるよ!」ということがあれば、ご教示いただけるとありがたいです。


参考1:Ruby リファレンス
https://docs.ruby-lang.org/ja/latest/method/Time/i/yday.html
参考2:生年月日から年齢を求める
http://rubytips86.hatenablog.com/entry/2014/03/27/101209

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

【初心者向】配列とハッシュ取り出し方、活用の仕方

Railsを何人かに教えていて、配列の取り出し方が怪しいのではないかと思えたので
勉強会資料として書きます。

配列というのはデータを複数入れることができるものです。
例えば@basic1 = [1, 2, 4, 8, 16, 32]
この時、複数入った中身を取り出すには

number = @basic1[0]
のように書く。

[0]というのがインデックス番号になって、0番目から始まります
今回の場合なら
0番目が1、1番目が2、2番目が4、3番目が8、4番目が16、5番目が32
となります。

ビューに表示するときは例えば

<h1>基本1</h1>
<div>
<h2>basic1</h2>
  <% @basic1.each do |b| %>
    <%= b %><br>
  <% end %>
</div>

/sample-blog/app/controllers/users_controller.rb
  def basic1
    @basic1 = [1, 2, 4, 8, 16, 32]
  end

image.png

同じパターンで

/sample-blog/app/controllers/users_controller.rb
  def basic1
    @basic2 = ["aaa", "bbb", "ccc", "ddd"]
  end
<h2>basic2</h2>
<div>
  <% @basic2.each do |b| %>
    <%= b %><br>
  <% end %>
</div>

image.png

配列の中に配列

少し複雑な場合

image.png

def basic1
    @basic3 = [[1, 2, 3],[3, 4, 5]]
end
<h2>basic3</h2>
<div>
  <% @basic3.each do |basic| %>
    <p><%= basic %></p>
  <% end %>
</div>

繰り返し1回目はindexが1繰り返し2回目はインデックスが1
indexが0のところは[1, 2, 3] 1のところは[3, 4, 5]

今度は全部表示させたい(取り出したい)場合

image.png

<h2>basic3-2</h2>
<div>
  <% @basic3.each do |basic| %>
    <% basic.each do |b| %>
      <%= b %><br>
    <% end %>
  <% end %>
</div>

eachの繰り返しの中にeachがあります。
1回目のループでbasic = [1, 2, 3]が取り出されるのでこれをさらにループにかけると1 2 3
2回目のループでbasic = [3, 4, 5]が取り出されるのでこれをさらにループにかけると3 4 5
と中身を取り出すことができます。

配列の中にハッシュ

@basic5 = [
      {name: "佐藤", mail: "satho@gmail.com"},
      {name: "田中", mail: "tanaka@yahoo.co.jp"}
    ]

一つ目の要素が{name: "佐藤", mail: "satho@gmail.com"}
二つ目の要素が{name: "田中", mail: "tanaka@yahoo.co.jp"}

以下のように取り出すことができます。

<h2>basic5-1</h2>
<div>
  <% @basic5.each do |b| %>
    <p><%= b[:name] %></p>
    <p><%= b[:mail] %></p>
  <% end %>
</div>

<h2>basic5-2</h2>
<div>
    <p><%= @basic5[0][:name] %></p>
    <p><%= @basic5[1][:mail] %></p>
</div>

image.png

こんな感じで取り出すことができます。

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

モデル データ取得

テーブルからデータを取得します

モデルから取得したデータはコントローラーに送られます。

その為にはActiveRecordクラスが必要です

メソッド 用途
all テーブルの全てのデータを取得する
find テーブルのレコードの内、ある1つのデータを取得する
new クラスのレコードを生成する
save クラスのレコードを保存する
post_controller.rb
def index

@post = Post.all #大文字Pからスタート 
@post_2 = Post.find(1) #1番のレコードを取得

以上でモデル内からデータを取得し、コントロール内で定義しているインスタンス変数に代入した

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

Rails Tutorial Memo #2

自分用の備忘録です.

第8章 基本的なログイン機構

セッション

HTTPはリクエストが終わると何もかも忘れて次回最初からやり直す健忘症的なプロトコル.

フラッシュ

flash[:danger]だとフラッシュメッセージが表示されてから他のページに移動してもフラッシュメッセージが残ってしまう.解決するためにはflash.now[:danger]を使う.

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

モデル作成

モデルの作成手順

ターミナル
rails g model モデル名

マイグレーションファイルが作成される

マイグレーションファイルとは.....
モデル内のテーブルを設計することができるファイル
例)モデル内のカラム名の変更

def made
  create_table :posts do |t| #Postテーブルを作成する
      t.text :content      #t.型の名前 :カラム名
      t.timestamps           #Railsが独自に用意している特別なもの
  end
end

マイグレーションファイル記載後、データベースに記載内容を反映させる

rails db:migrate 

テーブルに記載事項が追加さてます。

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

インスタンス変数

インスタンス変数

コントローラー内で定義したインスタンス変数は対応するビューファイルでも使える

def index
@post = "hey"
end

@post がインスタンス変数

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

【Rails】websocket_railsで開発していたチャットをActionCableにリプレース

はじめに

こんばんは。弊社の直近の仕事で行なっていたwebsocket_railsで開発していたユーザーチャットの機能をActionCableにリプレースする話でも執筆しようと思います。

なぜActionCableにリプレースするのか

単純に、もうそろそろ限界かなと思った次第です。

WebSocketとは

通常、クライアントがWebサーバと通信を行う際にはHTTPの手順に基づいて通信を行いますが、HTTPはステートレス(ステート: 状態という事をさす事が多い?)なプロトコルなため、どれだけリクエストを送っても過去のステートを保持しないため、サーバから同じレスポンスが返ってくる仕様です。勿論、現在のWebアプリケーションでは過去のステートを保持しなければならない事情の事の方が多いため、Cookie情報を付加したりして、過去のステートをサーバに渡す事もしています。

また、一度HTTP通信が完了すると、その通信は閉じられるため、基本的には片方向の通信しかできません(TCPのコネクションとは別の模様、HTTP1.1からそれまでTCPのコネクション閉じていたものが、閉じないようになった)。
そのため、XMLHTTPRequestやHTTPロングポーリングなどの複数のHTTP通信を介して双方向通信をしているように見せかける方法はあるのですが、如何せん実装は複雑になる傾向があります。

そこで、HTTPの通信を拡張させてTCPのようにハンドシェイクを行い、HTTP上で仮想的な通信経路(コネクション)を実現し、その中で双方向通信ができるようにしようと提案されたのがWebSocketです。

Railsは5.0以降においてActionCableと呼ばれるJSによるクライアントサイドとRailsによるサーバサイド両方のWebSocketフレームワークを提供できる機能が標準化されており、Rails5.0にアップグレードして以降利用したいツールでした(なお、Rails5.0以前はwebsocket-railsが主流だった?)。

今回は、websocket-railsで開発していたチャット機能をActionCableにリプレースします。
続いて、ActionCableの概要を書きます。

ActionCableの概要

参考文献があるのですが、ActionCableはどうも用語が多すぎて複雑です。パッと読んだ理解と下手なポンチ絵なので、間違いがあれば指摘をお願いします。

Action Cableの概要

このポンチ絵は、
https://railsguides.jp/action_cable_overview.html#%E7%94%A8%E8%AA%9E%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6
https://railsguides.jp/action_cable_overview.html#pub-sub%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6を参考に書きました。

要は、コネクションを確立させるためにチャネル(データを伝送する経路に)クライアントが加入すると、クライアントはサブスクライバという名前になり、パブリッシャ(情報を伝送する者)から受け取ったデータをブロードキャストでサブスクライバに伝達させます。
この時、特定の個人宛にデータを送信するわけではないので、ブロードキャストを行うことにより、パブリッシャも後にサブスクライバとなり、データを受信することができます。

中々、用語の整理が複雑ですが、基本的な所を抑えてしまえば、後はJSとRailsのコードでWebSocketの通信を行える事ができるので、次に具体的なコードにいってみましょう。

ActionCableをRailsサーバと一緒に実行する

routes.rbに以下のように/websocketでWebSocketリクエストがListenできるようにします。

config/routes.rb
    mount ActionCable.server => "/websocket"

クライアントがWebSocketリクエストを送る

まずはActionCableサーバにWebSocketリクエストを送ります。createConsumerメソッドにより、WebSocketのリクエストを送ります。createConsumerの引数には直接サーバホスト名を書く事もできますが、今回はパス名のみを指定します。

consumer.js
//= require action_cable
//= require_self
(function() {
  this.App || (this.App = {});
  App.cable = ActionCable.createConsumer("/websocket");
}).call(this);

ActionCableのサーバサイドの概要

ユーザーチャット用に利用するチャネルを作成します。チャネルの初回作成時には、親チャネルとなるapp/channels/application_cable/channel.rbapplication_cable/channel.rbなどなどが作成されます。

% bundle exec rails g channel chat

初めに、サーバサイド側のコードを見てみましょう。

app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_for room
  end

  def speak(data)
    message_datas = data["message"]
    message = message_datas["message"]
    account_class_name = message_datas["account_class_name"]
    account_id = message_datas["account_id"]
    chat = Chat.create!(chat_room_id: room.id, sender_type: account_class_name,
      sender_id: account_id, message: message)
    ChatChannel.broadcast_to(room, chat: chat)
  end

  def remove(data)
    chat = Chat.find_by(id: data["message_id"])
    chat.destroy!
    ChatChannel.broadcast_to(room, delete_id: data["message_id"])
  end

  def update(data)
    message_datas = data["message"]
    chat = Chat.find_by(id: message_datas["message_id"])
    chat.update(message: message_datas["message"])
    ChatChannel.broadcast_to(room, edit_id: chat.id, edit_message: chat.message)
  end

  private


  def room
    (中略)
  end

  def my_account
    (中略)
  end

end

この内のdef subscribed のコードを見てみましょう。
subscribedはコンシューマがChatChannelにサブスクライブされたときに呼び出されるコードになります。
stream_for により、今後ユーザーチャットでブロードキャストをする際に何をトリガーとしてブロードキャストをするのが指定しています。
今回は、中身が1対1のチャットということもあり、ChatRoomのIDを入れて検索をかけたオブジェクトをルーティングのトリガーとしています。

次に、speakメソッドはクライアントがメッセージを送信した時に呼び出されるメソッドとしてここでは定義しています。受け取ったパラメーターからDBにChatデータを残しつつ、他のクライアントへブロードキャストします。この時、特定の個人を指定していないのがポイントです。

同じように、update, removeメソッドはクライアントがメッセージを更新・削除したときに呼び出されるメソッドとして定義しています。

続いて、クライアントコードを見てみましょう。

ActionCableのクライアントコードの概要

app/assets/channels/chat_room.js
//= require ../channels/chat_channel.js

document.addEventListener("DOMContentLoaded", () => {
  if ($(".chat_rooms").length) {
    let chat_json = JSON.parse(*****);
    let account_class_name = *****
    let account_id = *****

    const reply_button = *****;
    reply_button.addEventListener("click", () => {
      let message = {};
      message["message"] = document.getElementById("chat_message").value;
      message["account_class_name"] = account_class_name;
      message["account_id"]= account_id;
      App.room.speak(message);
    });
    App.room = App.cable.subscriptions.create({ channel: "ChatChannel", room_id: *****, room_type: *****, account_class_name: account_class_name, account_id: account_id}, {
      connected: function() {
        this.appendChats(chat_json);
      },
      appendChats(data) {
        const html = this.createChats(data);
        const element = document.getElementById("chat-area");
        element.insertAdjacentHTML("beforeend", html);
      },
      updateChat(data) {
        let message = *****;
        message.innerText = data["edit_message"];
      },
      removeChat(message_id) {
        let remove_chat = *****;
        remove_chat.remove();
      },
      createChat(data) {
        (中略)
        return chat_line;
      },
      createChats(data) {
        let chat_area = [];
        let chats = []
        chats.push(`<div class="chat-message-container" style="max-height: 651px;">`);
        chats.push(`<div id="chat-list">`)
        (中略)
        return chats.join("");
      },
      speak: function(message) {
        return this.perform("speak", {
          message: message
        });
      },
      update: function(message) {
        return this.perform("update", {
          message: message
        });
      },
      remove: function(message_id) {
        return this.perform("remove", {
          message_id: message_id
        });
      },
      received: function(chat) {
        if(chat["chat"] != undefined) {
          this.appendChat(chat["chat"]);
        } else if(chat["edit_id"] != undefined) {
          this.updateChat(chat);
        } else if(chat["delete_id"] != undefined) {
          this.removeChat(chat["delete_id"]);
        }
      }
    });
  }
});

大分、マスキングしましたが、基本的には各々が書いたHTMLの操作に合わせてJSのメソッドを用意するような書き方で良いと思います。

App.room = App.cable.subscriptions.create({ channel: "ChatChannel", room_id: result["room_id"], room_type: result["room_type"], account_class_name: account_class_name, account_id: account_id}, {

指定のチャネルにサブスクリプションを作成することで、コンシューマがサブスクライバとして振る舞うことができます。
これ以降にも違う形でActionCableを使うのであれば、子チャネル用のサブスクライブメソッドを作ればいいわけです。
また、サブスクライバとなったクライアントからはサーバのパブリックメソッドが見えるため、公開されたパブリックメソッドをperformメソッドで呼びだすことができます。

      speak: function(message) {
        return this.perform("speak", {
          message: message
        });
      },
      update: function(message) {
        return this.perform("update", {
          message: message
        });
      },
      remove: function(message_id) {
        return this.perform("remove", {
          message_id: message_id
        });

各々のメソッドに合わせた、パラメータを付加してサーバのパブリックメソッドを呼び出します。
唯一、receivedはブロードキャストの通信を受け取った後に実行されるコールバックです。

      received: function(chat) {
        if(chat["chat"] != undefined) {
          this.appendChat(chat["chat"]);
        } else if(chat["edit_id"] != undefined) {
          this.updateChat(chat);
        } else if(chat["delete_id"] != undefined) {
          this.removeChat(chat["delete_id"]);
        }
      }

情報の伝達方法がブロードキャストのみとなっているので、サーバから受け取るブロードキャストの中身を各メソッド用にカスタマイズすることとしました。
これによって、ブロードキャストによって送られてきたデータの中身を確認することによって、クライアントの振る舞いを変えることができます。

後は各Webアプリケーションの実装に合わせて、HTMLを動的に追加・更新・削除させて見栄えを変えてみてください。

NginxとProductionの設定

後は、Webサーバとproduction.rbの設定を行います。

production.rb
config.action_cable.allowed_request_origins = ['https://bitstar.tokyo']

ActionCableは、指定されていない送信元からのリクエストを受け付けていないため、配列の形で許可する送信元ホスト名を渡します。

/etc/nginx/conf.d/virtual.conf
server {
    listen 80;
    server_name bitstar.tokyo;

    root /var/www/bitstar/current/public;
    (中略)
    location /websocket {
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_pass http://puma/websocket;
    }

最後にNginxの設定です。NginxはロードバランサーからのアクセスをRailsサーバにわたすリバースプロキシの設定を行っているため、/websocketのパスに対して、HTTPのプロトコルをWebSocketにスイッチングできるように設定をします。

これにより、RailsとActionCableの機能を一緒のPumaプロセスで動かすことができました。

まとめ

以上で、チャット機能をwebsocket_railsからActionCableにリプレースすることができました。
ActionCableに登場する人物の用語に関しては本当になんとかならなかったのかとちょっと疑問に思っています。
また、本当はwebsocket_railsからどうコードを移植したのか見せたかったのですが、実は修正前はHTMLの中で直接、<script>タグを埋め込んでその中でフォームを構成しており、非常に直しにくい印象を受けてしまい、「これは全捨ての方が良くないか?」という事でコード全捨てしてしまいました。
今だと、websocket_railsでWebSocketプロトコルによる機能を作るメリットもないと考えているので、リプレースを考えていらっしゃる人は一旦コード全捨てのアプローチを取った方がいいかもしれません。

それでは、また。

参考文献

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

Rails 流れ 其の壱

Rails 流れ


ターミナル rails new アプリケーション名 -オプション名
アプリケーションの作成

ターミナル rails db:create
データベースの作成

ターミナル rails g controller コントローラー名アクション名
コントローラーの作成

②ブラウザからURLが送信される

③ルーティングにて受け取ったURLに対してどのコントローラーのどのアクション(コントローラー内のメソッド)かを識別。

[アクション]

.index :詳細表示
.new :新規保存  
.create :保存
.edit :編集表示
.show :一覧表示
.update :更新
.delete :削除

ルーティングファイル内   HTTPメソッド "URL" => "コントローラー名#アクション名
 
. GET :ページを表示する操作のみを行う時
. POST :データを登録する操作をする時
. PUT :データを変更する操作をする時
. DELETE:データを削除する操作を行う時

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

Rails SQliteからmysqlへ変更(途中から)

アプリ作成後にmysqlに変更する方法

MySQLを起動して接続の確認

$ mysql.server start

起動後、接続できるか確認

mysql -u root -p

Railsで利用するユーザーを作成します。

create user ユーザー名@'localhost' identified by 'パスワード';
例 (create user railsuser@'localhost' identified by 'railspass';)

ユーザーが作成されたことが確認

select User,Host from mysql.user;

権限の付与を行います

grant all on *.* to ユーザー名@'localhost';

設定ファイルの変更

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: ユーザー名
  password: パスワード
  host: localhost

development:
  <<: *default
  database: sample_ec #データベース名

Gemfileに記述

gem 'mysql2'
$ bundle

mysqlにDBの追加とテーブル作成

$ bundle exec rake db:create
$ bundle exec rake db:migrate

これで完了

mysqlユーザーの削除方法

mysql> drop user ユーザー名@'localhost';

最初からmysqlを使用する場合

rails new アプリケーション名 -d mysql
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サーバエンジニアから始める、Swiftロードマップ

こんにちは
taskey サーバエンジニアの田代です。

ふとSwiftを学ぶことになり、その時の軌跡をまとめておこうと思いnoteに残しました。

対象者: サーバエンジニアだけど、Clientのことをもっとわかってあげたい方、こまい修正位やっちゃいたい方

社内マガジンに載せる都合上noteにまとめました。
内容はこちらをご閲覧下さい。

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

Ruby on Rails チュートリアル1周目

Railsチュートリアルを一周してみましたので簡単な感想とこれから学習する人向けにポイントなどをまとめようと思います。

まず、Railsチュートリアルをいきなり始めるのではなくもっと基礎的な学習を前にやっておいたほうがスムーズに理解できます。

学習カリキュラムについてはこちらの方が細かく書いてくれてます。
https://qiita.com/saboyutaka/items/1a8c40e105e93ac6856a

フロントエンド

私自身はWebデザイナーということもあり飛ばしていたのですが、
HTML/CSS(Sass)/Javascript
Jquery
Bootstrap
についてはすでに理解していました。

IT基礎知識

どうということはないのですがサーティファイの情報処理技術者能力検定試験もとっているので基礎知識も一応あるつもりです。
https://www.sikaku.gr.jp/js/jss/

Git, Command Line, SQL

Git, Command Line, SQLについては
Progate
ドットインストールで学習

Ruby, Ruby on Railsの基礎

さらに
Ruby
Ruby on Railsもこちらで一周しておきました。

書籍ではこちらをやってみました。
Railsの教科書
たった1日で基本が身に付く! Ruby on Rails 超入門
ゼロからわかる Ruby 超入門

Rails チュートリアル前に

下記サービスの登録をしておくとスムーズにすすめることができます。

Github、もしくはBitbucketの登録

Gitの操作を行うにあたってリポジトリーを登録する必要性があるのでやっとくといいでしょう。

Herokuの登録

フリープランでいいのでクレジット登録しておくと後のsendgridからのメールで認証するときに必要になります。

AWSの登録

Cloud9ではじめているならアカウントできていると思いますが、
画像を保存するときはS3を使うので先に登録しとくとよいでしょう。

Rails チュートリアル後

1周終えるだけボリュームがすごいのでかなりの学習時間がかかります。
その間に挫折する人も多いと思うのでおすすめは動画か質面ができるサービスと一緒にやるといいと思います。

1周を終えたあとは2周めやるもよし、サービスを作るとよしなのですが
何を作ったらいいのかわからない、もうちょっと勉強したいという方には

Techpit
クローンサイトを作れるカリキュラムがあります。
Rails チュートリアルに比べるとかなり簡単につくれますが機能が簡単すぎて物足らないとおもいますので
Rails チュートリアルと組み合わせながらやるのがいいかもしれません。

e-Navigator
作ったサービスのコードレビューをしてくれるサービスです。
現役エンジニアがレビューを行ってくれるので実践経験がない方にはちょうどいいかもしれません。

私はフロントエンド(Vue.js)のほうも勉強しているのでBackendの部分はもう一度Railsチュートリアルを進め、フロントの部分をうまいこと組み合わせて見ようと思います。

以上ですが、Railsチュートリアル1周目のまとめと感想でした。

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

人工知能系・機械学習系・AI系のオススメAPIまとめ

Watson API

提供:IBM
リンク: https://www.ibm.com/watson/jp-ja/developercloud/services-catalog.html
機能: 検索、テキスト分析、画像認識、音声認識、音声読み上げ、翻訳、心理分析、感情分析、データ分析プラットホーム

Vision AI

提供:Google
リンク: https://cloud.google.com/vision/
機能: クラウドやエッジにある画像を分析

Cloud Speech-to-Text API

提供:Google
リンク: https://cloud.google.com/speech-to-text/
機能: 機械学習によって音声をテキストに変換、短時間音声にも長時間音声にも対応

Cloud AutoML

提供: Google
リンク: https://cloud.google.com/automl/
機能: カスタム ML モデルを迅速かつ簡単にトレーニング

dialogflow

提供: Google
リンク:https://dialogflow.com/
機能: 顧客からの予約・オーダーの受付や問い合わせに自然言語プロセッサ(チャットボット)で対応をするAPI

Wit

提供: Wit.ai
リンク: https://wit.ai/
機能: 自然言語処理

AWS 機械学習

提供: AWS
リンク: https://aws.amazon.com/jp/machine-learning/
機能: 音声,高度なテキスト分析,ドキュメント分析,翻訳,文字起こし,対話型エージェント

参考

http://smsurf.app-rox.com/api/

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

rails-tutorial第3章

branchについて

$ git branch
これにより今いるブランチを確認することができる。基本的にmasterブランチは常に安定しなければいけないので、新規機能を作るときは必ずブランチを切る。

$ git checkout -b static-pages
-bはブランチを切るということ。このコマンドにより、masterブランチから離れる。

コントローラー

$ rails generate controller StaticPages home help
上記はStaticPagesコントローラーを作りhomeアクションとhelpアクションを追加する。さらにそれに対応するviewやテストを用意するという意味。また、rails g controllerコマンドは必ずコントローラー名を複数形にして書くこと。

テストについて

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
  end

end

上記はrails g controllerコマンドを使うと自動生成されるテストコード。
スクリプト言語とテストは切っても切り離せない。

test "should get home" do
    get static_pages_home_url
    assert_response :success
  end

例えば上記は
get static_pages_home_url:static_pagesコントローラーのhomeアクションにリクエストを送った時に、
assert_response :success :必ずなんらかのレスポンスが返ってくるはずだという意味。

ちなみにテスト駆動開発は、まずあるべき姿をテストコードで書く。テスト失敗する。テストが成功するようにコードを足していく、そしてリファクタリングを行うというスタイル。

staticpagesコントローラのタイトルをテストしてみよう

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

書くテスト3行目に足されたのは、
assert_selectはそのページのセレクトタグの中は以下のようになってますか?というもの

3) Failure:
StaticPagesControllerTest#test_should_get_home [/home/ec2-user/environment/sample_app/test/controllers/static_pages_controller_test.rb:8]:
<Home | Ruby on Rails Tutorial Sample App> expected but was
<SampleApp>..
Expected 0 to be >= 1.

3 runs, 6 assertions, 3 failures, 0 errors, 0 skips

ちなみにテストを実行した結果。
最後の行は3つのテストが走って、6つのアサーションがある中で、3つが失敗したよーって意味。

初めてのリファクタリング

<% provide(:title, "Home") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>
    <p>
      This is the home page for the
      <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
      sample application.
    </p>
  </body>
</html>

<% provide(:title, "Home") %>この部分は:titleに"Home"を代入している。

<%= yield(:title) %>はtitleをyieldメソッドで引っ張ってきてるという意味。

これにより、provideメソッドを除く、ドクタイプ宣言からheadタグまでが共通化できた、というかbodyタグの内側以外は全部同じなので外側にまとめようってこと。
<%=yield%>の中に各テンプレートが代入されるので、各テンプレートはbodyタグの内側だけ書いていれば良い。

その結果application.html.erbに共通のコードをまとめる。

<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

yield(:title)のtitleは各テンプレートから引っ張ってくるよーってこと。

branch のマージ

トピックブランチで作業したら最後にmasterブランチにマージしてあげる必要がある。

トピックブランチ内で
$ git add -A
$ git commit -m "Finish static pages"

$ git checkout master
ここでマスターブランチに移動。この時点でgit logをしてもトピックブランチで設定したコミットメッセージは表示されない。
$ git merge static-pages
これでトピックブランチの内容をmasterブランチに反映できる。

この状態でgit push heroku master を実行すればデプロイできる。

minitest reporters

railstestでgreenやredを表示するときの設定

test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require "minitest/reporters"
Minitest::Reporters.use!

class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.yml for all tests
  # in alphabetical order.
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

require 'minitest/reporters'
Minitest::Reporters.user!
この2行を追加する。

Guardによるテストの自動化

guardは何かファイルが更新されたら〜〜の処理を実行するというもの。
これをminitestと合わせると、ファイルを変えた時に勝手にテストを実行してくれる。

1.$ bundle exec guard init
を実行して初期化する。

2.$ sudo yum install -y tmux
cloud9の場合のコマンド。これによりguardの通知を受け取るのに必要なtmuxを有効にする。

3.生成されたguardfileの先頭の行に
guard :minitest, spring: "bin/rails test", all_on_start: false do
を加える。

4.gitignoreの最後の行に

# Ignore Spring files.
/spring/*.pid

を加える。

5.bundle exec guardを実行する。

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

deviseでログイン状態からsign_up(登録)画面に遷移した際のリダイレクト先

2種類のユーザ権限があるサービスで、ログイン状態かsign_up(登録)画面にした際に、想定してなかった画面にいリダイレクトされてこまったという話

構成

hoge_user, fuga_userという2種類のユーザがあります。それぞれ個別の認証が必要です。

また、それぞれのnamespaceでroot設定しています。

# routes.rb
    namespace :hoge_users do
      root to: 'tops#show'
      ~~
    end

    namespace :fuga_user do
      root to: 'tops#show'
      ~~
    end

さらに、親のルートではhoge_userのルートに遷移するように設定しています。

問題

fuga_userでログイン済状態でfuga_userのsign_up(登録)画面に遷移しようとするとhoge_userトップにリダイレクトされてしまう。

ログを見ると以下のように遷移していました。

  • fuga_user/new (sign_up)
  • /
  • hoge_user/top

ログイン済の場合親のルートにリダイレクトされたため、そこから更にhoge_userルートにリダイレクトされてました。

想定としては各ユーザのルート設定したとこにリダイレクトされると思ってたので困った。

調査

deviseのコードを確認しました。

  • newではrequire_no_authenticationが呼ばれログイン状態を確認する
  • ログイン済ならafter_sign_in_path_forを呼ばれる

なるほど。after_sign_in_path_forをoverrideしてないので親ルートにリダイレクトされると。

しかもこのアプリ構成上application_controllerではなく個別のcontrollerでoverrideする必要がある。

→ 2種類のそれぞれのリダイレクト先を設定したいので
session_controllerではすでにやってた。今回はregistration_conroller

対応

全然DRYじゃないんだけど、それぞれのユーザ配下のsession_controller, registration_controllerafter_sign_in_path_for をoverrideした。

これ以上増えるようだったらmixinするなり共通化します。

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

【Rails】Remember meチェックボックスと永続ログインのテスト【Rails Tutorial 9章まとめ】

Remember meチェックボックス

永続ログインするか否かを選択できるチェックボックスを、ログイン画面に実装する。

チェックボックス用フォーム

チェックボックスはユーザー情報入力用のフォームと同じ要領で作成できる。

app/views/sessions/new.html.erb
  <%= form_for(:session, url: login_path) do |f| %>
    .
    .
    .
    <%= f.label :remember_me, class: "checkbox inline" do %>
      <%= f.check_box :remember_me %>
      <span>Remember me on this computer</span>
    <% end %>

    <%= f.submit "Log in", class: "btn btn-primary" %>
  <% end %>

チェックボックスから送信される値はparams[:session][:remember_me]に入っている。
チェックすれば1、チェックを外せば0である。
1であればremember(user)メソッドを呼び出して永続ログインし、0であればforget(user)メソッドを呼び出して一時ログインするように、Sessionsコントローラのcreateアクションを変更する。

app/controllers/sessions_controller.rb
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

ここで使われている三項演算子は、if文と同じ意味である。

条件式 ? 処理A : 処理B
# if-else文と同じ
if 条件式
  処理A
else
  処理B
end

永続ログイン機能のテスト

永続ログインがちゃんと機能するか、テストを書く。

log_in_asテストヘルパー

Sessionsコントローラのlog_inヘルパーと同じ機能を持つ、テスト用のlog_in_asヘルパーメソッドを定義する。
ただし、統合テストではsessionメソッドが使えないようなので、統合テスト用に同じ名前のメソッドを作り、共にtest/test_helper.rbに置いておく。
こうすると、単体テストか統合テストかに関わらずlog_in_asメソッドでログインをテストできる。

test/test_helper.rb
class ActiveSupport::TestCase
  fixtures :all

  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end

  # テストユーザーとしてログインする
  def log_in_as(user)
    session[:user_id] = user.id
  end
end

class ActionDispatch::IntegrationTest

  # テストユーザーとしてログインする(統合テスト用)
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
end

第二引数にpasswordキーをわざわざつけて、テスト用ユーザーに合わせてpasswordを変えれるようにしているようだが、あまり意味はないんじゃないかと思う。
remember_meキーは値を変更できるようにすることで、チェックボックスのチェック有り無しに対応できるようにしている。

Remember meチェックボックスのテスト

チェックボックスのオンオフに対応する2つのテストを書く。

test/integration/users_login_test.rb
 test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies['remember_token']
  end

  test "login without remembering" do
    # クッキーを保存してログイン
    log_in_as(@user, remember_me: '1')
    delete logout_path
    # クッキーを削除してログイン
    log_in_as(@user, remember_me: '0')
    assert_empty cookies['remember_token']
  end

チェック無しでのログインでは、一度チェック有りでログインした後ログアウトし、cookiesの値が0→1→0と変化していることを確認している。
なお、テスト内ではcookies[:remember_token]と書けず、cookies['remember_token']と書かないといけない謎の仕様があるらしい。

assignsメソッド

テスト内ではUserモデルの仮想の属性であるremember_tokenにはアクセスできないが、assignsメソッドを使うとできるようになる。
今は割愛する。

current_userのテスト

current_userメソッドでは、session[:user_id]が存在しない場合、cookies.signed[:user_id]が存在するか確認し、存在すれば該当するユーザーを探して現在のユーザーとして返していた。
また、その際にcookiesのトークンとUserオブジェクトのトークンが一致するか検証していた。
この2点をテストする。
(ここではテスト忘れを防ぐテクニックが紹介されているが、今は割愛する。)

app/helpers/sessions_helper.rb
  # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

Sessionsヘルパー用のテストを作成する。

$ touch test/helpers/sessions_helper_test.rb

テストは以下のようになる。

test/helpers/sessions_helper_test.rb
require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end

まずテスト用のユーザーにremember(user)メソッドを使い、トークンの作成とremember_digestへの保存、cookiesへのユーザーIDとトークンの保存を行なっている。
cookies[:user_id]が存在し、sessions[:user_id]が存在しない状態である。

一つ目のテストは現在のユーザーが取得でき、それがテスト用ユーザーと一致するかを確認している。
current_userにはその後ログインしてsession[:user_id]にユーザーIDを入れる処理があるので、それも確認しておく。

二つ目のテストは、テスト用ユーザーのトークンに異なるトークンを入れてみて、現在のユーザーが取得できるかを確認している。

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

【Rails】cookiesによる永続的なログイン【Rails Tutorial 9章まとめ】

永続ログインとcookies

一時的なログインの維持にはsessionメソッドを使用する。
しかし、ブラウザを閉じると自動的にログアウトしてしまう。

ログイン状態を維持するためには、cookiesメソッドと、記憶トークンと呼ばれるデータを使用する。
記憶トークンはユーザーごとに固有な、パスワードのようなものである。
具体的な手順はこうなる。
①ユーザーごとに記憶トークンを作成する。
②記憶トークンをハッシュ化してUserモデルのカラム(remember_digest)に保存する。
③cookiesに記憶トークンとユーザーIDを入れる。
④ユーザーIDが入ったcookiesを受け取ったら、そのIDをもとにデータベースからユーザーを探し出し、そのremember_digestカラムに保存されているハッシュ値と、cookiesに入っているトークンをハッシュ化した値が一致するか確認する。

記憶トークンの生成と保存

remember_digestカラム

マイグレーションファイルを作成して、Userモデルにremember_digestカラムを追加する。
このカラムには、記憶トークンをハッシュ化して保存する。

$ rails generate migration add_remember_digest_to_users remember_digest:string
db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddRememberDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :remember_digest, :string
  end
end

記憶トークンの生成

記憶トークンはどんな文字列でもよいが、パスワードと違って個人が覚えておく必要がなく、かつセキュリティ上ランダムな文字列であることが望ましい。
ランダムな文字列を生成するために、ここではSecureRandomモジュールにあるurlsafe_base64メソッドを使う。

$ rails console
>> SecureRandom.urlsafe_base64
=> "q5lt38hQDc_959PVoo6b7A"

これを使って、トークンを生成するメソッドをUserモデルに定義する。

app/models/user.rb
  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

このメソッドを個々のUserオブジェクトで使用することはないので、クラスメソッドとしている。

rememberインスタンスメソッド

User.new_tokenメソッドで生成したトークンをユーザーに結びつけ、remember_digestカラムにハッシュ化して保存するrememberメソッドを定義する。

ところで、パスワードを保存する場合は次のような手順を踏んでいた。
①passwordというUserモデルの仮想の属性に生のパスワードを入れる。
つまり、@user.password = "foobar"のようにしていた。
②それがbcryptとhas_secure_passwordによってハッシュ化されてpassword_digestカラムに保存される。

これをトークンでも同じようにするのだが、トークンにはbcryptのような便利なものが無いので、今回はpassword属性に相当する仮想のremember_token属性を自分で作らねばならない。
仮想の属性を作るためには、attr_accessorを使う。

app/models/user.rb
attr_accessor :remember_token
.
.
.
 # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

attr_accessorで作った仮想の属性をメソッドの中で使うには、self.を頭につける。
(ところで、@remember_tokenではダメなんだろうか?)
update_attributeメソッドを使って、remember_digestカラムにハッシュ化したremember_tokenを代入する。

ログイン状態の保持

cookiesとrememberヘルパーメソッド

記憶トークンを作成し、各ユーザーのデータベースに保存できるようになったので、次はcookiesメソッドに記憶トークンとユーザーIDを入れる。
session[:user_id]にユーザーIDを入れたのと同様に、cookies[:user_id]にユーザーIDを、cookies[:remember_token]にremember_tokenを入れる。

cookies[:user_id] = user.id
cookies[:remember_token] = user.remember_token

ここで、cookiesを永続化するために.permanentメソッドを使用する。
また、ユーザーIDはセキュリティ上の問題から.signedメソッドを使用して暗号化する。
これは署名つきcookieと呼ばれる。

cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token

署名つきcookieは自動で複合化されるので、そのままユーザー検索に使える。

User.find_by(id: cookies.signed[:user_id])

これをSessionsコントローラのログイン部分で使用するために、Sessionsコントローラのヘルパーメソッドとして定義する。

app/helpers/sessions_helper.rb
  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

ここで非常にややこしいと思うのだが、このrememberヘルパーメソッドはデータベースにトークンを保存するために定義したrememberメソッドとは別物である。
前者はSessionsコントローラのアクション内で単独で使うが、後者はUserオブジェクトにつけて使うインスタンスメソッドである。

このメソッドは、まず後者のrememberインスタンスメソッドでユーザーとトークンを紐付け(remember_digestカラムにトークンをハッシュ化して保存)、その後cookiesにユーザーIDとトークンを入れている。

記憶トークンの照合

cookiesに入ったトークンをハッシュ化して、Userオブジェクトのremember_digestカラムに入っているハッシュ値と比較する。
これが少し分かりにくく感じるが...
①cookiesには生のトークンを入れる。
②remember_digestにはハッシュ化されたトークンが入っている。
③cookiesの生のトークンをハッシュ化してみて、それが②と一致するか確かめる。
ということらしい。
これはログイン機能で使用したauthenticateメソッドと同じである。
authenticateメソッドは次のように定義されている。

BCrypt::Password.new(password_digest)is_password?(unencrypted_password)

左の()内にあるのはpassword_digestカラムに入っているハッシュ化されたパスワードである。
右の()内にあるのは生のパスワードである。

これをトークンに当てはめるとこうなる。

BCrypt::Password.new(remember_digest).is_password?(remember_token)

これをもとに、authenticated?メソッドをUserモデルに定義する。

app/models/user.rb
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

ここで使われているremember_tokenはこのauthenticated?メソッドでのみ使用できるローカル変数で、attr_accessorで作成したUserモデルの仮の属性ではない。
(これはややこしいので、適宜名前を変えた方がよいと思う。)
左の(remember_digest)はUserモデルのremember_digest属性のことで、self.remember_digestと同じである。

永続的なログインの実装

必要なメソッドが揃ったので、Sessionsコントローラのcreateアクションを編集して、永続ログインを実装する。
ログインした後、rememberヘルパーメソッドを使用する。

app/controllers/sessions_controller.rb
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

current_userメソッドの修正

永続ログイン機能を実装できたが、一つ問題がある。
現在ログインしているユーザーを返すcurrent_userメソッドは、session[:user_id]からユーザーを探している。
ログインフォームからログインした場合はsessionにもcookiesにもユーザーIDを入れるので問題ない。
しかし、その後ブラウザを閉じると、cookiesは残るがsessionが消えてしまうので現在のユーザーが取り出せなくなる。
つまりログインしているのに現在のユーザーを見つけられないことになる。

これを、sessionが無い場合はcookiesからユーザーを探すように修正する。

app/helpers/sessions_helper.rb
  # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

cookiesからユーザーを見つけたら、(この時点ではログインできていないはずなので)ログインする。
ここは、ログイン時にemailからユーザーを見つけ、パスワードをauthenticateメソッドで照合するのと全く同じ要領である。
cookies[:user_id]からユーザーを見つけ、トークンをauthenticated?メソッドで照合する。
最後に、現在のユーザーを返す。

なお、sessionとcookiesは繰り返し処理を省くために、if文の条件式でローカル変数user_idに入れてから使っている。

永続セッションからのログアウト

永続ログイン状態を解除するために、永続ログインで作った2つのrememberメソッドにそれぞれ対応し、逆の操作をする2つのforgetメソッドを定義して、ログアウト処理に組み込む。

forgetインスタンスメソッド

Userモデルのrememberメソッドでは、Userオブジェクトのremember_digest属性にトークンをハッシュ化して保存した。
forgetメソッドはその逆で、remember_digest属性を空にする。

app/models/user.rb
  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end

forgetヘルパーメソッド

Sessionsヘルパーのremember(user)メソッドでは、rememberインスタンスメソッドを呼び出した後、cookiesにユーザーIDとトークンを入れた。
forget(user)メソッドはその逆である。

app/helpers/sessions_helper.rb
  # 永続的セッションを破棄する
   def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

log_outメソッドの修正

最後に、log_outメソッドにforget(user)メソッドを追加すればログアウト処理が完成する。

app/helpers/sessions_helper.rb
  # 現在のユーザーをログアウトする
   def log_out
    forget current_user
    session.delete(:user_id)
    @current_user = nil
  end

2つのバグ

完成した永続ログイン機能は、小さなバグが2つあるらしい。
…が、今のところ重要ではなさそうなので割愛する。

永続ログインのテスト

テスト部分は別記事にまとめることにする。

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

スクレイピング

とは

Web上のHTMLからデータを取ってくる処理

【その1】gemをインストール

Gemfile
gem 'mechanize'
ターミナル
bundle install


【その2】Mechanizeクラスを使うことを宣言

scraping.rb
require 'mechanize'


【その3】インスタンス生成

scraping.rb
require 'mechanize'

agent = Mechanize.new


【その4】URLからそのページのHTMLを取得

get("URL")

scraping.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("https://bluelog2.herokuapp.com/")


【その5】要素を取得

search('要素')

scraping.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("https://bluelog2.herokuapp.com/")
elements = page.search('#bluelog')

タグ指定('h1')やクラス指定('.manbow')もできます。

複数続けて、ここの中のここ!みたいなこともできます。
('h2 a')→h2タグの中のaタグを指定


【その6】テキストや属性値を取得

inner_textで中のテキスト

scraping.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("https://bluelog2.herokuapp.com/")
elements = page.search('#bluelog')

elements.each do |element|
  puts element.inner_text
end



[:属性]でその属性値

scraping.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("https://bluelog2.herokuapp.com/")
elements = page.search('#bluelog')

elements.each do |element|
  puts element[:href]
end

実際にやってみる

適当なファイルを作って、以下をコピペ

scraping.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("https://bluelog2.herokuapp.com/")
elements = page.search('#bluelog')

elements.each do |element|
  puts element.inner_text
end



ファイルを置いたディレクトリに移動し

ターミナル
ruby scraping.rb

を実行

ターミナル
BlueLog

と表示されたら成功

ちなみにこのサイトは僕が作ったスキューバダイビングの記録をつけるアプリケーションです!



ではまた!

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

Rails Tutorial Memo #1

自分用の備忘録です.

第7章 ユーザー登録

Rails.env.development?

Railsオブジェクトには環境の論理値(boolean)を取るenvという属性がある.
Rails.env.test?はテスト環境ではtrueを返し,それ以外の環境ではfalseを返す.
rails crails sはデフォルトではdevelopmentが使用されるが,他の環境を明示的に実行することもできる.

  • コンソールをtest環境で起動
    rails c test
  • サーバーをproduction環境で起動
    rails s --environment production

debugger

直接的にデバッグできる.
アプリケーションの中でよく分からない挙動が起きた時にdebuggerを差し込んで調べる.
トラブルが起こっていそうなコードの近くに差し込むのがコツ.

helper

ある動作を処理する場合にメソッド化して扱えるようにRailsにあらかじめ組み込まれた機能. link_toform_withlabel_forimage_tagなどは全てhelper

フォームHTML

<%= f.label :name %>
<%= f.text_field :name %>

<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />

を生成する

SSL

ローカルのサーバからネットワークに流れる前に,大事な情報を暗号化する技術.

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