railsのroutesのid以外にする方法

railsはdefaultのresourcesは:idになっています。

config/routes.rb

  resources :users

URIのパターンです。

                          Prefix Verb     URI Pattern                                            Controller#Action
                        new_user GET      /users/new(.:format)                                   users#new
                       edit_user GET      /users/:id/edit(.:format)                              users#edit
                            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

ただ、id以外にしたい場合があります。

routesにparamをつけてid以外に変更する

その場合はparamsを付け足せばいいです。

config/routes.rb

  resources :users, param: :name
                           users GET      /users(.:format)                                       users#index
                                 POST     /users(.:format)                                       users#create
                        new_user GET      /users/new(.:format)                                   users#new
                       edit_user GET      /users/:name/edit(.:format)                            users#edit
                            user GET      /users/:name(.:format)                                 users#show
                                 PATCH    /users/:name(.:format)                                 users#update
                                 PUT      /users/:name(.:format)                                 users#update
                                 DELETE   /users/:name(.:format)                                 users#destroy

https://github.com/rails/rails/blob/b5db73076914e7103466bd76bec785cbcfe88875/actionpack/lib/action_dispatch/routing/mapper.rb#L469

linkを貼る場合に楽をする

そして、この時のlinkを貼る場合にdefaultでnameにしたい場合があります。

@user = User.first
user_path(@user)

通常だと、この場合はidになるので、/users/1になります。

これをnameに変更します。

model/User.rb

def to_param
  name
end

これで/users/fooになります。

注意点

ただし、これをやる場合はnameがuniqにしておけないとバグの温床になります。 例えば、nameにfooが二人いた場合です。 だいたいcontrollerに書くのは下記のようなコードだと思います。

def show
  @user = User.find_by(params[:name])
  ...
end

find_byは最初の一つしか取得しないので、二つ以上あると予期せぬ挙動になります。

    def find_by(arg, *args)
      where(arg, *args).take
    rescue ::RangeError
      nil
    end

https://github.com/rails/rails/blob/c6f12715f1887c06f778a32330f59822ca77df20/activerecord/lib/active_record/relation/finder_methods.rb#L77

そのため、DB/モデルにuniq制約をつけるのを忘れないようにしましょう。

以上です。

railsでssl設定をした場合にしておいた方がいい設定

ssl対応をやってみました。

はてなブログssl対応を行っているし、今となっては当たり前になりましたね。

なぜ、そんなことが起こっているかというと、chromeで安全なサイトではないという警告が出るからですね。

そんなsslですが、let's encryptで行うことが増えているのかなとは思っています。

それについて、後日まとめることができたらまとめます。

今回はsslをした後のrailsの設定を見ていきたいと思います。

cookieにsecure属性をつける

cookieにsecure属性をつけることで、cookieも暗号化されます。 平文だと、万が一盗まれるようなことがあるとまずいので、secure属性をつけるのがいいでしょう。

config/initializers/session_store.rb

Rails.application.config.session_store(secure: Rails.env.production?)

nginxのリバースプロキシにssl通信のheaderをつけてあげる

railsforce_sslを利用しても、httpsと認識してくれません。 その原因は、httpsと認証するheaderがないからです。

https://github.com/rack/rack/blob/rack-1.5/lib/rack/request.rb#L70

    def scheme
      if @env['HTTPS'] == 'on'
        'https'
      elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
        'https'
      elsif @env['HTTP_X_FORWARDED_SCHEME']
        @env['HTTP_X_FORWARDED_SCHEME']
      elsif @env['HTTP_X_FORWARDED_PROTO']
        @env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
      else
        @env["rack.url_scheme"]
      end
    end

    def ssl?
      scheme == 'https'
    end

こうなっております。

schemeのどれでもいいので、headerとしてリバースプロキシから渡してあげましょう。

    location @app {
      ...
      proxy_set_header X-Forwarded-Proto $scheme;←追加
      ...
    }

ここまで来たらリダイレクト処理をnginx側で行います。 わざわざアプリケーション側でリダイレクト処理をするのは無駄な作業です。

  server {
    listen 80;
    rewrite ^ https://$server_name$request_uri? permanent;
  }

個人的には、rails側の設定もforce_sslで合わせました。 これで誰が見ても、設定がわかると思うので。

config/environments/production.rb

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

設定にもcookieにsecure属性をつけた方がいいよって書いてますね。

以上です。

参考

https://qiita.com/masarakki/items/e498d257a2105d055281

railsのARに対するpresent?とexists?のパフォーマンスの差

exists?の方がいいですという指摘を受けた。

なので、ここで確認する。

[4] pry(main)> Work.where(id: [*1..100]).exists?
  Work Exists (0.5ms)  SELECT  1 AS one FROM `works` WHERE `works`.`id` IN (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, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100) LIMIT 1
=> true
[5] pry(main)> Work.where(id: [*1..100]).present?
  Work Load (0.8ms)  SELECT `works`.* FROM `works` WHERE `works`.`id` IN (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, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)
=> true

exists?の方はLIMIT 1がついてある。

ということは、ARの結果が複数ある場合はexists?の方が高速ですね。

whereの後で存在を確認する場合はexists?を利用すべきだな。

これからは気をつけよう。

後日談

この後の展開が続くなら、present?の方がいいです

present?

- if @work.episodes.present?
  - @work.episodes.each.with_index(1) do |episode, i|
  Episode Load (0.4ms)  SELECT `episodes`.* FROM `episodes` WHERE `episodes`.`work_id` = 1

こういう場合ですね。 cacheした値がそのまま使用されます。

exists?の場合

- if @work.episodes.exists?
  - @work.episodes.each.with_index(1) do |episode, i|
  Episode Exists (0.3ms)  SELECT  1 AS one FROM `episodes` WHERE `episodes`.`work_id` = 1 LIMIT 1
  Episode Load (0.4ms)  SELECT `episodes`.* FROM `episodes` WHERE `episodes`.`work_id` = 1

ということは、ARの存在を確認するだけで、そこから先は展開しない場合ですね。

考えられる場所は、モデル層とかなのかな。

Viewで確認してよかった。

ということで、使用する場所はきちんと考えないとダメですね。

nginxのmoduleを追加する

nginxにはmoduleという概念があります。

これは、各機能がmodule単位で実装されており、moduleを組み合わせた構成になっています。

moduleには「静的module」と「動的module」の二種類があります。

「静的module」はビルドした時にしか組み込みができません。

「動的module」はビルドし直すことなく、追加することができます。

この概念自体はApacheからあります。

そして、nginxは昔は「動的module」をサポートしていませんでした。

https://heartbeats.jp/hbblog/2016/02/nginx-dynamic-modules.html

1.9.11で対応したようです。

ただし、一部だけになります。

下記は動的に組み込みができるものです。

Certified Dynamic Modules - NGINX

moduleは、上記以外には、サードパーティー製のものがあります(OSSに感謝)

https://www.nginx.com/resources/wiki/modules/

動的に組み込めれるものもあれば、静的に組み込むしかないものもあります。

ということは、きちんと最初で使用するであろうものは、用意しておくほうがいいですね。

ソースコードからビルドする

僕はmoduleを追加するためにソースコードからビルドしています。

ソースコードのダウンロードからやります。

deployer@ik1-324-22232:wget http://nginx.org/download/nginx-1.13.5.tar.gz
deployer@ik1-324-22232:tar xvf nginx-1.13.5.tar.gz
deployer@ik1-324-22232:cd nginx-1.13.5

まずは現状確認

deployer@ik1-324-22232:~$ sudo nginx -V
nginx version: nginx/1.13.5
built by gcc 6.3.0 20170516 (Debian 6.3.0-18)

何も入っていません。

なんでこうなっているのかというと、自分はModuleを簡単に追加できると思っていたからです。

ここからhttpsを使おうとすると、nginx_http_ssl_moduleがほしいです。

最初から入れておけばよかった。

ということで追加します。

ついでにwith-http_stub_status_moduleも追加します。

deployer@ik1-324-22232:/tmp/nginx-1.13.5$ sudo ./configure --with-http_ssl_module --with-http_stub_status_module --prefix=/usr/local/nginx --sbin-
path=/usr/sbin/nginx

deployer@ik1-324-22232:make
deployer@ik1-324-22232:make install

これでパッケージをもう一度ビルドできました。

deployer@ik1-324-22232:/tmp/nginx-1.13.5$ sudo nginx -V
nginx version: nginx/1.13.5
built by gcc 6.3.0 20170516 (Debian 6.3.0-18)
built with OpenSSL 1.1.0f  25 May 2017
TLS SNI support enabled
configure arguments: --with-http_ssl_module --with-http_stub_status_module --prefix=/usr/local/nginx --sbin-path=/usr/sbin/nginx

結論

原則動的に追加できないなど、nginxの特徴を押さえておくべきだった・・・orz

debianでmysqlの最新版をインストールする

何も考えずにapt-get install mysql-serverとすると、5.5系になります。

パッケージを更新しないといけません。

https://dev.mysql.com/downloads/repo/apt/

ここに書いてある場所からダウンロードして、更新します。

#wget https://dev.mysql.com/get/mysql-apt-config_0.8.7-1_all.deb

#dpkg -i mysql-apt-config_0.8.7-1_all.deb←更新する

あとはいつも通りです。

#apt-get update
#apt-get install mysql-server

参考

https://techglimpse.com/install-mysql-v5-7-debian-tutorial/

管理画面に他人の人がアクセスされてきた場合の対処方法

何も考えずにリダイレクトをしていました。

リダイレクトが悪い理由

リダイレクトをするということは、ページがあるということがバレます。

ページがないのなら、404を返すのがベターです。

ということは、404ページを作成して、それを返すのがいいです。

今は404ページを作成していないので、雑な返し方。

class Admin::ApplicationController < ApplicationController
  before_action :authenticate_admin!

  layout 'admin'

  private

  def authenticate_admin!
    head :not_found unless admin_signed_in?
  end
end

これを継承すれば、Adminに関しては、基本的にログインしていないと404になります。

ちょっとしたことですが、疑問に思いました。

皆さんはどうしているのでしょうか。

webpackerを使用した時にcssをどこに置くのかについて

webpackerを使用していて、cssをどうするか悩みました。

いや、そもそも何に悩んでいるの?という話だと思います。

jsファイルは原則javascript/packsに存在する→jsから読み込むcssファイルはどこに置く?→javascript/stylesを作成して、そこから使用するようにする→全部そこに置いたほうがよくない?

一番最後の全部javascript/stylesに置くというのは、webpackに全てを預けるということになります。

mastodonは全てをjavascriptに含んでいます。

https://github.com/tootsuite/mastodon/tree/master/app/javascript

メリット

  • ファイルの置く場所が固定できる→書くときに迷わない
  • 完全にsprocketsの呪縛を解くことができる(cssも)
  • ビルドを完全に一元化できる(webpackのみ)

デメリット

  • webpackが廃れた時の移行コストが高い。
  • imagesも置かないと、中途半端になる。

解決策

  • webpackとともにする
  • css/imagesはsprocketsを利用する

ここで後者を選びました。

理由

  • railsの資産が残る
    • image_tag asset_pack_path('logo_full.svg')と書かなくて良い(imageがpackってのはちょっと変な気がする)
  • 将来的な移行コスト(webpackはおそらく廃れるはず)

ただし、jsしか使用しない、cssjavascript/stylesを作成して、そこから使用するようにします。

こうすると、jsしかクラスはjs-xxxとして利用すれば、ネームスペースでぶつかることはなくなると思います。

想定されること

Q node_modules配下のcssはどうする?

A sprocketsに任せます

@import 'swiper/dist/css/swiper'

これはswiperというnpmのcssですが、sprocketsから読み込みをしています。

asstesのpathを確認します。

Rails.application.assets.paths
=> "/Users/xxx/rails/xxx/node_modules"

これで入っていないようでしたら、追加しましょう。

# config/application.rb
config.assets.paths << config.root.join("node_modules")

Q sprocketsのsassの変数共有したい

A webpackerの設定でなんとかできるはず(これは調べる)

結論

ルールを決めることが大事ですね。

railsでboolean値のvalidationをする方法

ちょっと罠にかかりました。

validates :publish, presence: true

いつも通りpresence: trueを書いていました。

こうすると、errorになります。

なぜ、そんなことが起こるかです。

rails/presence.rb at d57356bd5ad0d64ed3fb530d722f32107ea60cdf · rails/rails · GitHub

  def validates_presence_of(*attr_names)
    validates_with PresenceValidator, _merge_attributes(attr_names)
  end

https://github.com/rails/rails/blob/47eadb68bfcae1641b019e07e051aa39420685fb/activemodel/test/models/person_with_validator.rb#L6

class PresenceValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
  end
end

value.blank?になっていますね。

[1] pry(main)> false.blank?
=> true
[2] pry(main)> true.blank?
=> false

この結果、falseの場合に反応しています。

なので、別のvalidationを使います。

validates :publish, inclusion: { in: [true, false] }

これでtrue or falseが存在する場合に反応するようになります。

意外なところで存在を知った。

SQLでクロス集計をしてみる

SQLって表も簡単に作成できるんだなって最近感動しております。

今回やろうとしていることは、クロス集計です。

2000 10 10 8 9
2001 9 10 8 9
2002 10 10 8 10

年毎の作品を集計したいとかありそうじゃないですか。

ということで、やってみました。

テーブルの構成

mysql> show columns from works;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| title                  | varchar(255) | NO   |     | NULL    |                |
| season_year            | int(11)      | NO   |     | NULL    |                |
| season                 | int(11)      | NO   |     | NULL    |                |
| created_at             | datetime     | NO   |     | NULL    |                |
| updated_at             | datetime     | NO   |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+

season_yaerに2017

seasonに「0 = 春」「1 = 夏」「2 = 秋」「3 = 冬」

こういうデータが入っています。

SQL

春夏秋冬をどうするかということになります。

ここでcase文を使用します。

select season_year as 年,
      count(case when season = 0 then 1 else null end) as 春,
      count(case when season = 1 then 1 else null end) as 夏,
      count(case when season = 2 then 1 else null end) as 秋,
      count(case when season = 3 then 1 else null end) as 冬
  from works
 group by season_year;
      count(case when season = 0 then 1 else null end) as 春,←ここが大事。

case文であればcountして、なければcountしないnullを入れます。

+------+-----+-----+-----+-----+
| 年   | 春  | 夏  | 秋  | 冬  |
+------+-----+-----+-----+-----+
| 2000 |  17 |  15 |  12 |  16 |
| 2001 |  10 |   8 |  10 |  10 |
| 2002 |  12 |  16 |  10 |  11 |
| 2003 |  15 |  18 |  10 |  16 |
| 2004 |  21 |  15 |  15 |  14 |
| 2005 |  12 |  20 |  17 |  11 |
| 2006 |  19 |  18 |  14 |  15 |
| 2007 |  15 |  18 |  19 |  13 |
| 2008 |  16 |  18 |  27 |  20 |
| 2009 |  28 |  23 |  26 |  26 |
+------+-----+-----+-----+-----+

結果はこんな感じになります。

ここからさらに発展させます。

全体・前期・後期で知りたいんだってパターンですね。

select season_year as 年,
      count(case when season = 0 then 1 else null end) as 春,
      count(case when season = 1 then 1 else null end) as 夏,
      count(case when season = 2 then 1 else null end) as 秋,
      count(case when season = 3 then 1 else null end) as 冬,
      count(season) as 全体,
      count(case when season in(0, 1) then 1 else null end) as 前期,
      count(case when season in(2, 3) then 1 else null end) as 後期
  from works
 group by season_year;

inで条件分岐してあげます。

+------+-----+-----+-----+-----+--------+--------+--------+
| 年   | 春  | 夏  | 秋  | 冬  | 全体   | 前期   | 後期   |
+------+-----+-----+-----+-----+--------+--------+--------+
| 2000 |  17 |  15 |  12 |  16 |     60 |     32 |     28 |
| 2001 |  10 |   8 |  10 |  10 |     38 |     18 |     20 |
| 2002 |  12 |  16 |  10 |  11 |     49 |     28 |     21 |
| 2003 |  15 |  18 |  10 |  16 |     59 |     33 |     26 |
| 2004 |  21 |  15 |  15 |  14 |     65 |     36 |     29 |
| 2005 |  12 |  20 |  17 |  11 |     60 |     32 |     28 |
| 2006 |  19 |  18 |  14 |  15 |     66 |     37 |     29 |
| 2007 |  15 |  18 |  19 |  13 |     65 |     33 |     32 |
| 2008 |  16 |  18 |  27 |  20 |     81 |     34 |     47 |
| 2009 |  28 |  23 |  26 |  26 |    103 |     51 |     52 |
+------+-----+-----+-----+-----+--------+--------+--------+

以上です。

case文がこんな使い方あるとはなーって感じです。

Deviseでユーザー登録後のリダイレクト先を変更する

Deviseのカスタマイズに少し苦労しました。

大体標準通りに使えば問題ないのかもしれませんが、少し加工しようとするとちょっと調べないといけないですね。

今回行うことは、Deviseでユーザー登録後に、Thanksページに飛ばすことです。

リダイレクト自体は割と簡単なのですが、コントローラーにアクションを追加するのに手間取りましたorz

コントローラーをカスタマイズする

今回のDeviseはユーザーモデルをベースとして考えています。

まずはカスタマイズするために、下記のコマンドを打ち込みます。

rails generate devise:controllers Users

これでテンプレートが作成されるかと思います。

変更するコントローラーはclass Users::RegistrationsController < Devise::RegistrationsControllerになります。

thanksアクションを入れます。

class Users::RegistrationsController < Devise::RegistrationsController
  # before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

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

  # POST /resource
  def create
    super
  end

  # GET /resource/edit
  # def edit
  #   super
  # end

  # PUT /resource
  # def update
  #   super
  # end

  # DELETE /resource
  # def destroy
  #   super
  # end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  # def cancel
  #   super
  # end

  def thanks
  end

  # protected

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_sign_up_params
  #   devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  # end

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_account_update_params
  #   devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  # end

  # The path used after sign up.
  # def after_sign_up_path_for(resource)
  #   super(resource)
  # end

  # The path used after sign up for inactive accounts.
  # def after_inactive_sign_up_path_for(resource)
  #   super(resource)
  # end
end

routesを設定する

ここが少し戸惑いました。

  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

上記を追加して、registrationsの動作をusers/registartionsに設定します。

次に、アクションをthanksページのroutesを追加します。

  devise_scope :user do
    get 'users/thanks' => 'users/registrations#thanks'
  end

devise_scopeで設定しないと、request.env[“devise.mapping”]でroutesの設定がされません。

https://github.com/plataformatec/devise/blob/88724e10adaf9ffd1d8dbfbaadda2b9d40de756a/lib/devise/rails/routes.rb#L361

    def devise_scope(scope)
      constraint = lambda do |request|
        request.env["devise.mapping"] = Devise.mappings[scope]
        true
      end

      constraints(constraint) do
        yield
      end
    end

こんな常識知らないよって感じですが、設定しましょう。

ユーザー登録後のリダイレクト先を変更する

  def after_inactive_sign_up_path_for(resource)
    users_thanks_path
  end

users/registarationコメントアウトされていると思います。

ここに追加すれば終わりです。

まとめ

ドキュメントとソースコードを見よう。