最小限の機能で作成していく方法
よく、最小限の機能でリリースしろ!なんて巷で言われますよね。
けど、気がついたらガッツリ思い込みでガチガチに仕様を固めています。
自分はよくそうなるので、どうすればそうならないかを考えました。
というか、最小限の機能で作成する能力って技術の一つじゃないの?とすら、思っています。
だって、難しいから。。。
よく考えてみると後から追加できる形で作成するすればいいなとふと、気づきました。
よくある悩み
例えば、読書管理のサイトを作成しようとします。
読書管理するために、読書している状態を取得したいです。
- 読んだ
- 読みたい
という2つの状態で作成する方法。
- 読んだ
- 読書中
- 読みたい
という3つの状態で作成する方法があるとします。
作成者本人は読書中は微妙じゃない?と思いつつ、後者を選ぼうとしています。
なぜなら、選択肢が多い方が細かいデータを取得できて、ユーザーにとって便利だ!という悪魔の声があります。
3つのステータスがある場合
雑にやりましたが、セレクト形式になると思います。
この欠点は、二回クリックしないといけません。
「セレクトボックス」選択→「ステータス」選択
この後に待っているのは、状態をもっと細かく取った方がいいんじゃないの?という話になるでしょう。
2つのステータスの場合
では、最初から、「読んだ」「読みたい」の二つしかない状態にします。
デザインが雑なんで、あれですが、こちらの方は1クリックでできます。
「読んだ」を押すとチェックマークが入るアニメーションをつければ終わります。
ユーザーとしても、迷いが減ります。
そして、何よりも後から「読書中」は必要なら追加することができます!
一方、最初から3つのステータスで作成していた場合は、なかなか消すことができません。
なぜなら、ユーザーさんが使用している可能性があり、簡単に消すことはサイトの信頼性が失われます。
後から追加できる形で作成する
おそらく、自分の中では最小限の実装で作成するんだ・・・!と思いつつも実際はガッツリと組み込んで作成してしまいます。
自分がそうなので・・・
そして、ガッツリ組み込んで作業していく場合は、作業に没頭できるので、やった気になります。
後から追加できる形で作成すれば、反応を見ながら作成していくことができます。
なので、自分は後から追加できるから、今は作らなくてもいいやと思って、割り切って最小限で作成していきたいと思います。
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
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
そのため、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をつけてあげる
railsはforce_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属性をつけた方がいいよって書いてますね。
以上です。
参考
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
参考
管理画面に他人の人がアクセスされてきた場合の対処方法
何も考えずにリダイレクトをしていました。
リダイレクトが悪い理由
リダイレクトをするということは、ページがあるということがバレます。
ページがないのなら、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しか使用しない、cssはjavascript/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
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文がこんな使い方あるとはなーって感じです。