- 投稿日:2020-01-03T23:06:18+09:00
Rack入門 Rack Middleware編 (3/3)
前回 は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。目次
Rack Middlewareとは
はじめにややこしいことを言いますが、Rackはミドルウェア(Middleware)です。
アプリサーバーとフレームワーク間のやりとりを仲介しているため、ミドルウェアと呼ばれます。今回学ぶのはミドルウェアとは何か、ではなくてRack Middlewareについてです。
Rackには以下2つの概念があります。
Rack Application
- 前回学んだ、callメソッドを持つオブジェクトのことです
- StatusCode・Headers・Bodyの3つをレスポンスとして返します
- Rack Endpointとも呼ばれます
Rack Middleware
- 今回学ぶものです
- Rack Middlewareはcallメソッドを持つclassである必要があります
- Rack Applicationと違い、Responseを直接返すのではなく別の処理を呼び出しデータを加工するために使います
Rack Middlewareは、渡ってきたenv情報を加工し、次のmiddlewareまたはendpointに処理を引き渡すものです。
Hello Rack Middleware
Rack Middlewareはenv情報を加工し、次に引き渡すために利用します。
Bodyに「Hello Rack Middleware」と追加するだけのRack Middlewareを作成してみます。class App def call(env) [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]] end end # Rack Middlewareは以下条件を満たす必要がある # - classであること # - initializeでappを受け取ること # - callメソッドを実装し、Status/Headers/Bodyを返すこと (Rack Endpointと同じ条件) class HelloRackMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) fixed_body = ["Hello Rack Middleware!\n"] + body [status, headers, fixed_body] end end use HelloRackMiddleware run App.newRack Middlewareをclassとして作り、useメソッドを呼び出すことでmiddlewareを追加できます。
アプリを起動し、レスポンスを見てみます。$ curl http://localhost:9292/ * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Transfer-Encoding: chunked < Hello Rack Middleware! HELLO Rack Endpoint! * Connection #0 to host localhost left intact * Closing connection 0
Hello Rack Middleware!という文字列がBodyに追加されていますね。Middlewareはいくつでも追加できます。ただし、useの順序によってMiddlewareの動作順が異なることには注意が必要です。
class App def call(env) [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]] end end class HelloRackMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) fixed_body = ["Hello Rack Middleware!\n"] + body [status, headers, fixed_body] end end class AnotherMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) fixed_body = ["Another Middleware!\n"] + body [status, headers, fixed_body] end end use HelloRackMiddleware use AnotherMiddleware run App.newレスポンスは以下のとおりです。
Hello Rack Middleware! Another Middleware! HELLO Rack Endpoint!Content-Lengthを追加するMiddleware
もう少し実用的なmiddlewareを作ってみましょう。
Response HeaderにContent-Lengthを挿入するmiddlewareを作ります。class App def call(env) [200, { "Content-Type" => "text/plain" }, ["HELLO WORLD!", "Hello"]] end end class ContentLengthMiddleware def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) body_size = 0 body.each do |b| body_size += b.bytesize end headers["Content-Length"] = body_size.to_s [status, headers, body] end end use ContentLengthMiddleware run App.newアクセスしてみましょう。
$ curl -v http://localhost:9292/ * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Content-Length: 17 < * Connection #0 to host localhost left intact HELLO WORLD!Hello* Closing connection 0ちゃんとContent-Lengthが設定されていますね。
Rackにあらかじめ用意されているRack Middleware
Content-Lengthを設定するmiddlewareなど、よく使うと思われるMiddlewareはrack本家で実装されています。
いくつか主要なものを紹介します。
Rack::ContentLength
- https://github.com/rack/rack/blob/master/lib/rack/content_length.rb
- Bodyをみて、Content-Lengthヘッダーを設定します
Rack::Deflater
Rack::ETag
Rack::Session::Cookie
- https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb
- cookieベースのSession管理を行います
Rack::Reloader
- https://github.com/rack/rack/blob/master/lib/rack/reloader.rb
- リクエストが来た際に、実行中のRubyファイルが更新されていたら自動でリロードします(開発時に利用するもの)
その他のmiddlewareは https://github.com/rack/rack/tree/master/lib/rack を参照してください。
Railsで使われているRack Middleware
Railsもrackのプロトコルに従い作成されています。
Railsで実際に使われているmiddlewareを覗いてみます。$ rails new racktest ...省略... $ cd racktest $ bin/rake middleware Running via Spring preloader in process 10795 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 Racktest::Application.routesいくつかrackで実装されているMiddlewareも利用しているのがわかります。
Railsで利用しているMiddlewareについては、https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack にて詳しく説明されています。まとめ
- Rack Middlewareは、渡ってきたリクエスト/レスポンスを加工するために利用する
- Middlewareはcallメソッドを実装したclassである必要がある
Rackをより理解するためには
全3回にわたって、Rackの基本的な概念を簡単なアプリを作りながら学びました。
思ったよりシンプルな構造でしたね。よりRackを理解するための参考資料を、以下に記載しておきます。
Rack本家のExample
- https://github.com/rack/rack/blob/master/lib/rack/lobster.rb
- アクセスするとロブスターを返します。
bundle exec ruby lib/rack/lobsterで起動できますRack::Request,Rack::Responseを使って処理をラップしているので少し複雑ですが、基本的な考え方は今回学んだとおりですResponse#finishは https://github.com/rack/rack/blob/master/lib/rack/response.rb#L66 のとおりで、StatusCode/Headers/Body のペアを返します- (Paste has a Pony って、なんだろう...)
次世代のRackやWSGIを考えてみる
- https://qiita.com/kwatch/items/67657fef43666479bb99
- Rackの問題点について言及されています。
RackのHTTP/2サポートについて
- https://techracho.bpsinc.jp/hachi8833/2017_07_03/42195
- https://github.com/tenderlove/the_metal/issues/5
- Rackは今回学んだ通り、リクエストに対して1つのレスポンスを返します。しかしHTTP/2だとレスポンスは複数返さないといけないので、さてどうしたものか、という点について言及されています。
- 投稿日:2020-01-03T23:05:42+09:00
Rack入門 Rack Application編 (2/3)
前回 はRackが必要とされた背景と、基本的な概念について説明しました。
今回は実際にRackプロトコルを使いアプリサーバーと通信するプログラムを作りながら、Rackに関する理解を深めていきます。目次
Hello Rack Application
基本を理解するために、簡単なRackアプリケーションを作ってみます。ブラウザーからアクセスされたら、「Hello Rack!」と返すだけのシンプルなアプリです。
まずは以下Gemfileを用意し、bundle installをしてください。アプリサーバーはpumaを使います。
source "https://rubygems.org" gem "rack" gem "puma"次に
config.ruというファイルを作成します。拡張子がruですが、中身はrubyのコードです。class HelloRackApp # callメソッドはenvを受け取り、3つの値(StatusCode, Headers, Body)を配列として返す def call(env) [200, { "Content-Type" => "text/plain" }, ["Hello Rack!"]] end end # callメソッドを呼び出せるObjectをrunに渡し、rackアプリを起動する run HelloRackApp.newターミナル上で以下のコマンドを実行し、Rackアプリを起動します。
bundle exec rackup -s pumarackupコマンドはデフォルトではconfig.ruを読み込み、Rackアプリを起動します。
bundle exec rackup config.ruとしてファイルを指定することもできます。
-s pumaはアプリサーバーの指定です。指定しない場合はpumaかthinかwebrickのいずれかが使われます。$ bundle exec rackup -s puma Puma starting in single mode... * Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://127.0.0.1:9292 * Listening on tcp://[::1]:9292 Use Ctrl-C to stopport9292でRackアプリが起動しました。
pumaはWebサーバーとしての機能も持っているため、ブラウザーやコマンドからもアクセスできます。
curlでアクセスしてみます。$ curl -v http://localhost:9292 * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact Hello Rack!* Closing connection 0Hello Rack!とレスポンスが返ってきました。Statusは
200 OK、Headerには指定したContent-Type: text/plainも含まれています。ブラウザーからもアクセスしてみます。Rackアプリケーションの形式
Rackアプリはアプリサーバーと通信するために、以下の形式(プロトコル)にしたがって作成する必要があります。
- RackアプリはObjectであり、callメソッドを呼び出せること。callメソッドは引数を一つ受け取ること
- callメソッド呼び出し後、以下3つの値を配列として返すこと
- (HTTP) Status Code
- Headers
- Body
- Status Codeは3桁のHTTPのステータスコード(100以上の数値)であること
- Headersはeachメソッドを実装し、yieldの際にkey/valueのペアを渡すこと。key/valueは必ずStringであること
- Bodyはeachメソッドを実装し、yieldの際にStringを渡すこと
実際はさらに細かく仕様が決まっていますが、要はこれだけです。
Rackの詳細な仕様は https://github.com/rack/rack/blob/master/SPEC に記載されています。yieldの際にStringを渡すこと、というのがわかりにくいですね。
rubyのyieldについて復習しておきます。[1,2,3,4,5].each { |v| puts v }上記コードの実行結果は以下となります。
1 2 3 4 5この
vがyieldしたときに渡ってくる値です。eachは以下のように実装できます。class MyArray def initialize(values) @values = values end def each for v in @values yield v end end end MyArray.new([1,2,3,4,5]).each { |v| puts v }RackのHeadersはkey/valueの2つの値を渡しますが、この値がStringであれば良いです。
Bodyは、yieldの引数として渡す値がStringであれば良いですね。Rackアプリが満たすべき要求はこれだけです。
もう一度最初のRackアプリのコードを見てみます。class HelloRackApp def call(env) [ 200, # 1番目はStatusで、3桁の数値を返す { "Content-Type" => "text/plain" }, # 2番目はHeaders, Hashはeachメソッドを実装済 # key/valueはStringあること ["Hello Rack!"] # 3番目はBody, Arrayはeachメソッドを実装済み # 中身はStringであること ] end end run HelloRackApp.new上記の形式に従っていれば、立派なRackアプリケーションの完成です。
HelloRackAppはclassである必要はありません。callメソッドを持ったObjectであればよくて、Headers/Bodyもeachを持ってさえいればよいので、以下のコードでも動きます。
class MyHeader def initialize(bodies) @bodies = bodies end def each yield "Content-Type", "text/plain" yield "Content-Length", @bodies.join.bytesize.to_s # NOTE: ValueはStringにしないといけない end end class MyBody def initialize(bodies) @bodies = bodies end def each for v in @bodies yield v end end end # NOTE: procはcallで呼び出し可能 app = proc do |env| bodies = ["こんにちはRack!\n\n", "Hello Rack!\n"] [200, MyHeader.new(bodies), MyBody.new(bodies)] end run app$ curl -v http://localhost:9292 * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET / HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Content-Length: 34 < こんにちはRack! Hello Rack! * Connection #0 to host localhost left intact * Closing connection 0envを使ってリクエストを取り出す
callメソッドの引数envには、ブラウザーなどからのリクエストが格納されています。
以下のコードで中身を覗いてみます。class HelloRackApp def call(env) require "pp" pp env [200, {}, []] end end run HelloRackApp.newrackup後、サーバーにアクセスして結果を見てみます。
{"rack.version"=>[1, 3], "rack.errors"=> #<Rack::Lint::ErrorWrapper:0x00007f90a41c0c00 @error=#<IO:<STDERR>>>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "SCRIPT_NAME"=>"", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"puma 4.3.1 Mysterious Traveller", "GATEWAY_INTERFACE"=>"CGI/1.2", "REQUEST_METHOD"=>"GET", "REQUEST_PATH"=>"/", "REQUEST_URI"=>"/", "HTTP_VERSION"=>"HTTP/1.1", "HTTP_HOST"=>"localhost:9292", "HTTP_USER_AGENT"=>"curl/7.64.1", "HTTP_ACCEPT"=>"*/*", "puma.request_body_wait"=>0, "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"9292", "PATH_INFO"=>"/", "REMOTE_ADDR"=>"::1", "puma.socket"=>#<TCPSocket:fd 18, AF_INET6, ::1, 9292>, "rack.hijack?"=>true, "rack.hijack"=> #<Proc:0x00007f90a41c0fc0 /Users/nishio/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rack-2.0.8/lib/rack/lint.rb:525>, "rack.input"=> #<Rack::Lint::InputWrapper:0x00007f90a41c0c28 @input=#<Puma::NullIO:0x00007f90a585a8f8>>, "rack.url_scheme"=>"http", "rack.after_reply"=>[], "puma.config"=> #<Puma::Configuration:0x00007f90a41d7798> "rack.tempfiles"=>[] }envには以下の全てが混ざったデータがHash形式で格納されています。
HTTPリクエストヘッダー
- HTTP_VERSIONやHTTP_USER_AGENTなど、HTTPリクエストヘッダーの値がkey/value形式で入っている
- Keyは他の情報と区別するために、
HTTP_というprefixを付与しているHTTPリクエスト情報
- QUERY_STRING や REQUEST_URIなど、リクエストヘッダー以外のリクエスト情報が入っている
pumaやrackなどのアプリからの情報
- rack.version や puma.config など、各種アプリが付与した情報が入っている
envの値を利用することで、リクエストに応じたレスポンスを返せます。
試しに、以下アプリを作ってみます。
http://localhost:9292/helloにアクセスしたら、Hello!を返す- 上記以外のURIにアクセスしたら、
404 Not Foundを返すclass App def call(env) # NOTE: # REQUEST_PATHも利用できるが、実装されていないケースが存在する # PATH_INFOは必ず存在するので、urlはPATH_INFOを使って取り出す方が安全である # https://github.com/envato/jwt_signed_request/issues/15 path = env["PATH_INFO"] if path == "/hello" [200, { "Content-Type" => "text/plain" }, ["HELLO!"]] else [404, { "Content-Type" => "text/plain" }, ["404 Not Found"]] end end end run App.new上記アプリにアクセスしてみましょう。
$ curl -v http://localhost:9292/hello * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET /hello HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/plain < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact HELLO!* Closing connection 0$ curl -v http://localhost:9292/testtest * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 9292 (#0) > GET /testtest HTTP/1.1 > Host: localhost:9292 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 404 Not Found < Content-Type: text/plain < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact 404 Not Found* Closing connection 0ファイルを返すアプリを作成する
画像ファイルをレスポンスとして返すアプリを作ってみます。
RackのBodyは文字列でさえあればなんでもよいので、以下のように実装すれば画像を返せます。class App def call(env) path = "./neko.jpg" # 画像ファイルは好きなものを用意 file = File.open(path, "rb") image_data = [file.read] # Bodyは必ずeachメソッドを持つ必要があるのでArrayとする [200, { "Content-Type" => "image/jpeg" }, image_data] ensure file.close end end run App.new上記コードにブラウザーからアクセスしてみます。
確かにこれでも動きはしますが、ファイル全体を一度メモリ上に読み出してBodyに詰める必要があります。
読み込み効率が悪いので、レスポンスがしんどいですね。Rackはファイルをレスポンスすることも考え、以下のようなコードを記述できます。
class App def call(env) path = "./neko.jpg" # 画像ファイルは好きなものを用意 file = File.open(path, "rb") [200, { "Content-Type" => "image/jpeg" }, file] end end run App.newRubyのFileは以下メソッドを持っています。
eachメソッド
- eachを使えば、一定データ量ずつファイルから読み出せます
- 例えば
File.each(100)とすれば、ファイルを100バイトずつ読み出せますcloseメソッド
- ファイルはopenしたら必ずcloseする必要があります
Rackは、
closeメソッドを持ったオブジェクトをbodyとして渡すと、各種処理を実行後にcloseを呼び出してくれます。このように、Fileオブジェクトを渡すことを想定した仕様があらかじめ定義されています。
rackupコマンドを使わずrackアプリを起動する
最後に、rackupコマンドを使わずにアプリを起動するコードを紹介します。
$ bundle exec ruby config.ru実装は以下コードとなります。
class App def call(env) [200, { "Content-Type": "text/plain" }, ["Hello Rack!"]] end end require "rack" app = Rack::Builder.new do run App.new end require "rack/handler/puma" Rack::Handler::Puma.run app
rackやpuma関係のファイルをrequireし、Rack::Builderで初期化したものをPumaのHandlerに渡せばよいです。
rackupコマンドは、このあたりのファイル読み込みをあらかじめ自動で行ってくれる便利コマンドです。まとめ
- Rackアプリはあらかじめ決められたRackプロトコルにしたがって作成する
- Specの詳細は https://github.com/rack/rack/blob/master/SPEC に記載されている
- Rackアプリはcallメソッドを持ったオブジェクトであること
- callメソッドは、引数を1つ受け取り、StatusCode・Headers・Bodyのペアを返す
- callメソッドの引数(env)から、リクエスト情報を取り出せる
- 投稿日:2020-01-03T23:05:18+09:00
Rack入門 概念編 (1/3)
RailsやSinatraなどのRuby製Webフレームワークを利用されている方は、Rackというキーワードを一度は目にしたことがあるのではないでしょうか。
よく聞くけど詳しくは知らない、そんなやつがRackです。
今回は自分の知識の整理も兼ねて、Rackとは何ものなのかについて調べたメモを、ここに残します。長かったので、全3回に分割しています。
目次
Rackとは何か。ひとことで
- Webアプリケーションサーバーとアプリケーションを接続するための標準化されたインターフェースのこと
Rackとは何か。詳細
Rackの公式リポジトリでは、Rackは以下のように記載されています。
a modular Ruby webserver interface 訳) モジュール化されたRuby Webサーバーインターフェースそして、
Supported web serversとして PumaやUnicorn が上げられています。このWebサーバーという表現はいろいろとややこしいので、本稿ではPuma/Unicornのことは アプリケーションサーバー(アプリサーバー) と呼びます。
Rackはなぜ必要なのか。その前に基本的な概念についておさらいします。
アプリケーションサーバーとは
アプリケーションサーバーって、なんでしたでしょうか。
ここで一度振り返っておきます。Webサーバー、アプリサーバーの代表的なソフトウェアは以下のとおりです。
Webサーバー
- nginx, apache など
アプリケーションサーバー
- Rubyでは Puma, Unicorn など
- JavaではTomcat など
Webシステムの多くは、いわゆる3層アーキテクチャと呼ばれる構成で設計されており、Webサーバーとアプリサーバーは別に運用します。
以下のような概念図をよく目にするのではないでしょうか。DBサーバーについては、今回は関係ないので説明を割愛します。
別にこの構成に従わなくてもウェブシステムは運用できますが、システムの性能を引き出すために3層アーキテクチャを採用することが多いです。
Webサーバー
Webサーバーは、ユーザーからのリクエストを受け取り、処理結果をレスポンスとして返すソフトウェアのことです。
受け取ったリクエストはWebサーバー自身が処理することもありますし、そのまま処理をアプリサーバーに委譲することもあります。
静的なコンテンツ(画像やファイル)は、Webサーバーでデータを引っ張ってきて返すことが多いです。
ビジネスロジックはRubyなどで書いたアプリに処理を委譲して、処理結果をレスポンスとして返すことが多いです。nginxなどのWebサーバーは、大量のアクセスが来ても素早く効率的にさばくための機構を持っています。
またSSL通信やデータを圧縮して返すなど、Webの面倒事を引き受けてくれるソフトウェアです。アプリケーションサーバー
アプリサーバーは、アプリをあらかじめ起動しておき、リクエストが来たらすばやく処理をするためのサーバーです。
アプリサーバーがないと、どうなるでしょうか。
昔のWebシステムでは、アプリサーバーは立てませんでした。
CGI全盛期は、Webサーバーにリクエストが来た後、プログラムを都度起動していました。
リクエストのたびにプログラムの起動から始めるというのは、巨大なアプリケーションになればなるほど起動コストが無視できないですよね。なので、あらかじめプログラムをメモリ上に読み込んでおき、アクセスが来たら処理を高速に返せるようアプリサーバーを用意します。
PumaやUnicornなどのアプリサーバーは、Webサーバーとしての最低限の機能も持っています。
Webサーバーを立てずにPumaだけでもシステムを運用できます。
ただ、大量のアクセスを効率的にさばくことはPuma単体ではできない(そこはWebサーバーの役割としている)ので、一般的には前段にWebサーバーを立ててリクエストを処理をします。Rackはどこで使われ、なぜ必要か
Rackはアプリサーバーとアプリ(Webフレームワーク)間のデータのやり取りに利用します。
PumaやUnicornといったアプリサーバーは、特定のアプリ専用(たとえばRails専用)というわけではなく、RackのプロトコルにしたがっているWebフレームワークであれば何でも利用できます。
Rackに対応しているRuby製のフレームワークは、Railsの他にSinatra, Padrino, Hanamiなどがあります。
アプリサーバーとアプリ間の通信仕様を定めておく(=インターフェースの標準化をしておく)ことで、
アプリサーバーとアプリケーションフレームワークの組み合わせを自由に変えることができます。
Rails専用のアプリサーバーを作る、Sinatra専用のアプリサーバーを作るっていうのは大変ですよね。アプリ・フレームワーク間の標準インターフェースを作るという流れは、PythonのWSGI(Web Server Gateway Interface)がはじめました。
PythonはもともとWebフレームワークとフレームワーク専用のアプリサーバーが乱立していましたが、WSGIはアプリとサーバーのその組み合わせを自由に変えられるよう標準インターフェースを定めました。
これにより、移植性の高いアプリサーバー/フレームワークを作ることができるようになりました。Rackの細かいところはWSGIと異なりますが、このWSGIの影響を強く受けています。
まとめ
- アプリサーバーとは、アプリをあらかじめ起動しておきレスポンスを高速に返すためのサーバー
- Rackはアプリサーバーとフレームワーク(アプリ)間のインターフェースを定めている
- Rackのプロトコルに従うことで、アプリサーバーとWebフレームワークの組み合わせを自由に変えることができる
- 投稿日:2020-01-03T20:29:22+09:00
【Rails】devise_token_authで新規ユーザー登録時にUnpermitted parameters: が出たときの対処法
はじめに
gem devise_token_authを使っていて、新規ユーザー登録時に以下エラーが出たときの対処法を残します。
Unpermitted parameters: 保存されて欲しいカラム名自分でUserテーブルにカラムを追加して、保存しようとしたら
発生する内容です。今回、自分の場合は
:ageと:genderを追加しようとしたところ発生しました。(以下参照)Unpermitted parameters: :age, :genderこれを解決していきます。
環境
OS: macOS Catalina 10.15.1 zsh: 5.7.1 Ruby: 2.6.5 Rails: 6.0.2.1結論:
registrations_controllerに追記registrations_controllerclass Api::V1::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController private def sign_up_params # ここに :age, :genderを追記 params.permit(:name, :email, :age, :gender, :password, :password_confirmation) end def account_update_params params.permit(:name, :email) end end※反映するには
rails server再起動が必要です。理由
Strong Parameterではじかれてしまっているのが問題でした。
sign_up_paramsの中に追記することで、新規ユーザー登録のときだけ許可されるようになります。こいつも許可してあげてよ!という指定をすればOKですね。
おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
- 投稿日:2020-01-03T18:34:09+09:00
#Stripe API / サブスクリプションのスケジュール登録が開始済みでも予約中でも SubscriptionSchedule を Update してキャンセル登録する例 / #Ruby
points
- 1ヶ月サイクルのプラン1個だけをスケジュール登録して、あとから end_date = キャンセル日を登録する場合
- Subscription に対してであれば Update で cance_at を登録すれば良いのだが、SubscriptionSchedule が開始していない場合は、そもそも Subscription が発生していないため、できない
- どちらの場合も SubscriptionSchedule への update で APIリクエストを完結させたいが、SubscriptionSchedule はちょっと癖があるやつ
- SubscriptionSchedule が active = 開始している場合は、 current_phase.start_date を起点に、そこから数ヶ月後の end_date をぴったりと指定する。なぜなら中途半端な日時を指定すると、日割り計算的な請求が発生してしまうから。
- SubscriptionSchedule が not_started= 開始していない場合は、 未来に開始される最初のフェーズ = phasse の start_date を起点に、そこから数ヶ月後の end_date をぴったりと指定する。なぜなら中途半端な日時を指定すると、日割り計算的な請求が発生してしまうから。ちなみに現在フェーズの期間 ( current_phase ) はサブスクリプションの請求サイクルとは異なる。
- SubscriptionSchedule Update で phase 単位で iterations を指定することもできるだろうが、start_date / end_date を明確に与えてあげたほうが、場合によってはやりやすそうだ。いや、やりやすい方でお願いします。
Docs
- https://stripe.com/docs/billing/subscriptions/subscription-schedules
- https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases
- https://support.stripe.com/questions/create-update-and-schedule-subscriptions
参考 - Stripeの基本
日本正式リリースしたStripeを使ってサブスクリプション型決済システムを実装する - Qiita
https://qiita.com/tady/items/7617e62b2a5402ebd0fbStripe Billing 101 - Qiita
https://qiita.com/y_toku/items/235b5e7ee00792edcbbfStripe初心者のための基本的な使い方(Rails編) - Qiita
https://qiita.com/ryouzi/items/6ee8f277471aa3b02f7bDocs
- https://stripe.com/docs/billing/subscriptions/subscription-schedules
- https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases
- https://support.stripe.com/questions/create-update-and-schedule-subscriptions
Code
# Docs # https://stripe.com/docs/billing/subscriptions/subscription-schedules # https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases # # https://stripe.com/docs/api/subscription_schedules/release # https://support.stripe.com/questions/create-update-and-schedule-subscriptions require 'active_support/core_ext' require 'stripe' Stripe::api_key = ENV['STRIPE_SECRET_KEY'] [1.month, 3.months].each do |end_at_from| product1 = Stripe::Product.create(name: "Gold plan #{rand(9999999999)}") plan1 = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 5000, product: product1.id, usage_type: 'licensed') tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false) customer = Stripe::Customer.create payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01}) customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id) def put_subscription_schedule(subscription_schedule, message) puts '-' * 100 puts "Subscription Schedule" puts message puts '-' * 100 puts subscription_schedule puts '-' * 100 puts "https://dashboard.stripe.com/test/subscription_schedules/#{subscription_schedule.id}" end # https://stripe.com/docs/api/subscription_schedules/create soon_start_subscription_schedule = Stripe::SubscriptionSchedule.create( { customer: customer.id, start_date: Time.now.to_i + 5, default_settings: { default_payment_method: customer_payment_method.id, }, phases: [ { plans: [ { plan: plan1.id, quantity: 1 }, ], prorate: false, default_tax_rates: [tax_rate], }, ], } ) puts '-' * 100 puts "Wait until subscription schedule starts" puts '-' * 100 until soon_start_subscription_schedule.status == 'active' do soon_start_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(soon_start_subscription_schedule.id) puts soon_start_subscription_schedule.status sleep 2 end started_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(id: soon_start_subscription_schedule.id, expand: ['subscription']) future_subscription_schedule = Stripe::SubscriptionSchedule.create( { customer: customer.id, start_date: Time.now.to_i + 100*24*60*60, default_settings: { default_payment_method: customer_payment_method.id, }, phases: [ { plans: [ { plan: plan1.id, quantity: 1 }, ], prorate: false, default_tax_rates: [tax_rate], }, ], } ) # current_phase.start_date is not subscription cycle start_date def update_subscription_schedule(base_subscription_schedule) if base_subscription_schedule.status == 'active' start_date = base_subscription_schedule.current_phase.start_date elsif base_subscription_schedule.status == 'not_started' start_date = base_subscription_schedule.phases[0].start_date else raise end end_date = Time.at(start_date).since(3.months).to_i Stripe::SubscriptionSchedule.update( base_subscription_schedule.id, { # Set "cancel" not "releasse" end_behavior: 'cancel', phases: [ { plans: [ { plan: base_subscription_schedule.phases[0].plans[0].plan, quantity: base_subscription_schedule.phases[0].plans[0].quantity }, ], # prorate does not effect to Subscription or SubscriptionSchedule Canceling # prorate: true / false, default_tax_rates: base_subscription_schedule.phases[0].default_tax_rates.map(&:id), # For Update You must set start_date in first phase not in subscription schedule directly start_date: start_date, # If you do not want to get upcoming invoice on Subscription # Then you must specify end_date with Subscription natural cycle interval end_date: end_date }, ] } ) end updated_started_subscription_schedule = update_subscription_schedule(started_subscription_schedule) updated_future_subscription_schedule = update_subscription_schedule(future_subscription_schedule) put_subscription_schedule(updated_started_subscription_schedule, 'UPDATED') put_subscription_schedule(updated_future_subscription_schedule, 'UPDATED') end # NOTE # SubscriptionSchedule current_phase is not same as Subscription cycle # it is "phase" start_date and end_date # # If phase end_date is 3.months after then ... # # Time.at(updated_started_subscription_schedule.current_phase.start_date) # => 2020-01-02 17:44:23 +0900 # Time.at(updated_started_subscription_schedule.current_phase.end_date) # => 2020-04-02 17:44:23 +0900Started Subscription - cancel at current phase
Upcoming invoice leaves from dashboard
Not Started Subscription - cancel at first phase
Theres Starts Ends in both in future
Theres upcoming invoice becauce Schedule will start in future
Not current invoice because Schedule not startedStarted Subscription - cancel at future phase
Change end_date in code and run Case
Not Started Subscription - cancel at not first phase
Change end_date in code and run Case
Result
- This is run code resutls example but not correctly same as Image capture images in this article.
---------------------------------------------------------------------------------------------------- Wait until subscription schedule starts ---------------------------------------------------------------------------------------------------- not_started not_started not_started not_started not_started not_started active ---------------------------------------------------------------------------------------------------- Subscription Schedule CREATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955005, "current_phase": null, "customer": "cus_GTM5yQ0vRozYUW", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE", "default_source": null, "invoice_settings": null }, "end_behavior": "release", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNHCmti5jpytUA4yh9gmW", "object": "tax_rate", "active": true, "created": 1577954987, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1589187005, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5XKzi7c1qy8", "quantity": 1, "tax_rates": [ ] } ], "prorate": false, "start_date": 1586595005, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPNZCmti5jpytUWUBqK4lG", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNJCmti5jpytU8hFkMssH", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577954989, "current_phase": { "end_date": 1585817393, "start_date": 1577954993 }, "customer": "cus_GTM5yQ0vRozYUW", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNHCmti5jpytUA4yh9gmW", "object": "tax_rate", "active": true, "created": 1577954987, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1585817393, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5XKzi7c1qy8", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1577954993, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPNZCmti5jpytUoCQuoPaf", "status": "active", "subscription": "sub_GTM59xT4BS5E3j" } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNJCmti5jpytU8hFkMssH ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955005, "current_phase": null, "customer": "cus_GTM5yQ0vRozYUW", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNHCmti5jpytUA4yh9gmW", "object": "tax_rate", "active": true, "created": 1577954987, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1594457405, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5XKzi7c1qy8", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1586595005, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPNaCmti5jpytUnlvLBTke", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN ---------------------------------------------------------------------------------------------------- Wait until subscription schedule starts ---------------------------------------------------------------------------------------------------- not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started active ---------------------------------------------------------------------------------------------------- Subscription Schedule CREATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955067, "current_phase": null, "customer": "cus_GTM5Wlau0N9HHj", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk", "default_source": null, "invoice_settings": null }, "end_behavior": "release", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNbCmti5jpytUOMYY1jCE", "object": "tax_rate", "active": true, "created": 1577955007, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1589187067, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5akDaPHCKOr", "quantity": 1, "tax_rates": [ ] } ], "prorate": false, "start_date": 1586595067, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPOZCmti5jpytUsHM0JqYJ", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5a ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNdCmti5jpytUnvii1fUR", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955009, "current_phase": { "end_date": 1585817414, "start_date": 1577955014 }, "customer": "cus_GTM5Wlau0N9HHj", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNbCmti5jpytUOMYY1jCE", "object": "tax_rate", "active": true, "created": 1577955007, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1585817414, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5akDaPHCKOr", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1577955014, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPOaCmti5jpytU2OqMiUUC", "status": "active", "subscription": "sub_GTM64npF4L1rop" } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNdCmti5jpytUnvii1fUR ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955067, "current_phase": null, "customer": "cus_GTM5Wlau0N9HHj", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNbCmti5jpytUOMYY1jCE", "object": "tax_rate", "active": true, "created": 1577955007, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1594457467, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5akDaPHCKOr", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1586595067, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPObCmti5jpytU5eKvVg9W", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5aOriginal by Github issue
- 投稿日:2020-01-03T18:28:29+09:00
3ヶ月で Rails 4.2 → 5.2 にした軌跡
概要
資格スクエアの PM をやっています岩瀬と言います。資格スクエアは、法律系難関資格に特化したオンライン学習サービスです。
サービスを開始してから数年が経ち、1つの大きな問題がありました。それは Rails のメジャーアップグレードです。
長年の負債が溜まっていたので、なかなかメジャーアップグレードできずに苦しんでいたのですが、フリーランスのカルパスさん(yhirano55)の加入で一気に進みました。
カルパスさんが方針を考えて実装し、それをレビューでサポートしたラスタムさん(rastamhadi)はじめ、チーム一丸となって取り組んだ結果、わずか3ヶ月で成し遂げられるミラクルを目撃したので、その内容をまとめました。
記事の目的
テストカバレッジが低くても、Rails のメジャーアップグレードができるという知見を共有するためです。
背景
Rails メジャーアップグレードは適宜対応していくのが最善ですが、以下の課題があったため、作業が難航していた状態でした。
- サービス成長を優先していたため、Rails のメジャーアップグレードが後回しになっていた
- サービス開始から5年以上が経過していたため、負債が溜まりメンテナンスコストが高くなっていた
- サービス立ち上げ時はテストコードを書いていなかったため、カバレッジが 32 %と低い状態であった
ですが、Rails 6.0 が出るにあたって、いよいよ Rails 4 系のサポートが終了するため、早急に Rails 5 系に上げる必要がありました。
どうしようか悩んでいた時、ラスタムさんの紹介によってカルパスさんが加入して本格的に着手することになりました。
バージョン
以下は Rails のメジャーアップグレード作業に着手した際に使用していたバージョンです。
名称 バージョン Rails 4.2.11.1 Ruby 2.3.6 規模
資格スクエア は、2013年11月にサービスを立ち上げてから5年以上が経過しており、中規模と言えるような状態になっていました。規模の目安として、あくまで参考ですが2020年1月現在での各モジュールの数は以下になります。
名称 数 Controllers 177個 Models 190個 Code LOC 108,612行 戦略
上記の背景と規模を踏まえて、次の戦略で進めることにしました。
- メジャーアップグレードを優先して最小のテストカバレッジで進める
- テストでカバーできないものは動作確認で洗い出す
- 動作確認でも洗い出せなかったものは本番環境に反映後、即対応する
タイムライン
今回の取り組んだアップグレードのざっくりしたタイムラインは以下になります。
年月日 内容 2019年8月27日 Rails 5.0 へメジャーアップグレード作業開始 2019年10月17日 Rails 5.0.7.2 にアップグレード完了 2019年10月23日 Ruby 2.6.5 にアップグレード完了 2019年11月19日 Rails 5.2.3 にアップグレード完了 手順
Rails 4.2 → 5.0
それではポイントを押さえて、実際に行ったメジャーアップグレード手順を紹介します。
1. 足場の確保
まずは開発環境を整え、テスト実行の足場を確保しました。
- 落ちているテストを直す
- 開発用途の gem を更新する(lock_diff を活用して changelog を追う)
2. モデルとテーブル定義の不一致を解消する
モデルによるレコード生成をテストコードで担保したいのですが、そもそもモデルとテーブル定義が不一致だったので、それを解消しました。
- annotate でモデルとテーブル定義の制約条件(validation)が一致していることを確認しやすいようにする
- 開発環境と本番環境のテーブル定義を一致させる
- テーブルの NOT NULL 制約とモデルの presence validation を一致させる
- テーブルの unique index とモデルの uniqueness validation を一致させる
3. 全モデルに最低限の単体テストを書く
ここで全モデルのテストコードを書きました。単体テストを網羅するのはあまりにも大変なため、最低限レコード生成を担保するテストを整備しました。
- 全モデルの factory を定義する
- レコードの生成が valid となる最低限のテストを用意する
ex.
spec/models/certificate_spec.rbrequire 'rails_helper' RSpec.describe Certificate do describe 'factory' do it 'has a valid factory' do expect(build(:certificate)).to be_valid expect(create(:certificate)).to be_persisted end end end4. 管理画面に最低限の結合テストを書く
管理画面は gem を使わず、独自実装であったため結合テストで最低限画面表示できることを担保しました。
ex.
spec/features/admin/certificates_spec.rbrequire 'rails_helper' RSpec.feature '管理ツール 資格管理', type: :feature do scenario '一覧が閲覧できる' do prepare_logged_in_admin_user within(:css, '#sidebar') do click_link '資格管理' end expect(page).to have_current_path admin_certificates_path expect(page).to have_selector 'h1.page-header', text: '資格管理' end end5. Rails 5.0 の変更点に応じて修正する
Rails 4.2からRails 5.0へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。
- Rails 5.0 から全 Model は ApplicationRecord という抽象クラスを継承する形式のため変更する
- ActionController::Parameters は今後 HashWithIndifferentAccess を継承しないので対応する
- migrationファイルを置き換える
ex.
db/migrate/20150302014237_create_admin_users.rb- class CreateAdminUsers < ActiveRecord::Migration + class CreateAdminUsers < ActiveRecord::Migration[4.2]6. Rails 5.0 に上げる Pull Request を作成する
ここで Rails 5.0 に上げる目処が立ったので、 Pull Request を作成しました。
- gem の依存バージョンを解決する
- Rails 5.0.7.2 にアップグレードする
- 設定ファイルの更新(app:update)
- テストが実行できる
- テストを実行し、落ちているものを修正する
- すべての警告を抑止する
7. テスト環境で隅々まで動作確認する
最低限のテストコードでカバーしているため、テスト環境で動作確認を行いました。その動作確認の結果、エラーになった箇所を1つ1つ解決していきました。資格スクエアで遭遇したエラーは以下になります。
- Rails 5.0 では存在しないコールバックをスキップすると ArgumentError になるので不要なコールバックを削除する
- params をそのまま使うと Rails 5.0 からセキュリティの都合で ArgumentError になるため、hash に変更する
- Rails 5.0 で collection_check_boxes の hidden 要素が最後から最初に変わったため、修正する
8. Rails 5.0 を本番環境にデプロイする
いよいよ Rails 5.0 に変更できる準備が整ったのでデプロイしました。
何か問題が発生しても対応できるようにエンジニアメンバーが稼働している勤務時間帯に反映しました。9. 本番環境で発生した問題に対処する
実際、デプロイ後にやはり問題が発生しました。具体的には以下のものになります。
- 本番反映前の Rails 4.2 で作られたジョブを Rails 5.0 で動作させるために一時的にパッチを当てる
ex.
config/initializers/temporary_active_record_patch.rbclass ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter class MysqlDateTime < ActiveRecord::Type::DateTime end end
- Rails アップグレードに伴い、セッションが切れる問題が発生したためにセッションをクリアするコマンドを実行する
$ RAILS_ENV=production bundle exec rails r 'Rails.cache.clear'これで Rails 4.2 → 5.0 へメジャーアップグレード完了です。
Rails 5.0 → 5.2
Rails 5.0 にメジャーアップグレードできましたが、ここで立ち止まらず引き続きマイナーアップグレードを行いました。それは Rails 5.2 まで上げると system spec が使えるようになり、javascript の動作をテストコードで担保できるようになるからです。
1. 整理整頓
メジャーアップグレードで把握した使用 gem の内、不要なものは削除するなど、メンテコストを下げました。
2. Rails 5.1 の変更点に応じて修正する
Rails 5.0からRails 5.1へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。
- ActionController::Parameters における HashWithIndifferentAccess のメソッド呼び出しのサポートが削除されたので対応する
- render :text を render :plain に変更する
- render :nothing を head :ok に変更する
3. ついでに API のテストカバレッジを上げて整理する
Rails で API を担っている領域のテストカバレッジが低かったので、requests spec で担保しました。
- 正常系と異常系の requests spec を整備する
- 不要なエンドポイントを削除する
4. Rails 5.1 に上げる Pull Request を作成する
ここで Rails 5.1 に上げる目処が立ったので、 Pull Request を作成しました。
- gem の依存バージョンを解決する
- Rails 5.1 にアップグレードする
- 設定ファイルの更新(app:update)
- テストが実行できる
- テストを実行し、落ちているものを修正する
- すべての警告を抑止する
5. Rails 5.2 の変更点に応じて修正する
引き続きRails 5.1からRails 5.2へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。
- uniq は Rails 5.1 で削除されるので distinct に置換する
6. Rails 5.2 に上げる Pull Request を作成する
ここで Rails 5.2 に上げる目処が立ったので、 Pull Request を作成しました。
- gem の依存バージョンを解決する
- Rails 5.2 にアップグレードする
- 設定ファイルの更新(app:update)
- テストが実行できる
- テストを実行し、落ちているものを修正する
- すべての警告を抑止する
7. テスト環境で隅々まで動作確認する
この段階でテストカバレッジは 47 % と以前よりはだいぶ上がっていますが、
まだまだテストコードで担保できていない機能があるので、テスト環境で動作確認を行いました。8. Rails 5.2 を本番環境にデプロイする
Rails 5.2 に変更できる準備が整ったのでデプロイしました。
9. 本番環境で発生した問題に対処する
Rails 5.2 のデプロイ後は特にエラーは発生しませんでした。Rails 5.0 の時と同様にセッションが切れる問題は再度発生することが予想されたので対応しました。
これで Rails 5.0 → 5.2 へマイナーアップグレード完了です。
その後
Rails 6.0 にはすぐに上げることができるのですが、特に使いたい機能があるわけではなかったので、ここで一旦立ち止まり強化することにしました。対応したことは以下になります。
- system spec による結合テストの整備(2020年1月時点でカバレッジ 53 %)
- Dependabot によるバージョン追従
- CI の実行速度の改善と整備
- CD の開発
まとめ
Rails 4.2 から 5.2 までアップグレードした3ヶ月の軌跡をまとめました。
最初、自分が資格スクエアをメジャーアップグレードしようと考えた時は、正直テストカバレッジが低いので相当難しいなという印象でした。
しかし、効果的な戦略と的確な変更によってわずか3ヶ月で成し遂げられた状況を見て、とても感動しました。そしてシステム面では新しい機能が使用できたり、パフォーマンスが向上したりと様々な恩恵を受けています。
この方法が自分たちと同じようにメジャーアップグレードに悩んでいる方々にとって参考になれば幸いです。
- 投稿日:2020-01-03T18:20:42+09:00
Stripe API / Update SubscriptionSchedule and set cancel_at / started case and not started case / without "adjusted" Upcoming invoice / Ruby code example / #Stripe #API #Ruby
Docs
- https://stripe.com/docs/billing/subscriptions/subscription-schedules
- https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases
- https://support.stripe.com/questions/create-update-and-schedule-subscriptions
Code
# Docs # https://stripe.com/docs/billing/subscriptions/subscription-schedules # https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases # # https://stripe.com/docs/api/subscription_schedules/release # https://support.stripe.com/questions/create-update-and-schedule-subscriptions require 'active_support/core_ext' require 'stripe' Stripe::api_key = ENV['STRIPE_SECRET_KEY'] [1.month, 3.months].each do |end_at_from| product1 = Stripe::Product.create(name: "Gold plan #{rand(9999999999)}") plan1 = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 5000, product: product1.id, usage_type: 'licensed') tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false) customer = Stripe::Customer.create payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01}) customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id) def put_subscription_schedule(subscription_schedule, message) puts '-' * 100 puts "Subscription Schedule" puts message puts '-' * 100 puts subscription_schedule puts '-' * 100 puts "https://dashboard.stripe.com/test/subscription_schedules/#{subscription_schedule.id}" end # https://stripe.com/docs/api/subscription_schedules/create soon_start_subscription_schedule = Stripe::SubscriptionSchedule.create( { customer: customer.id, start_date: Time.now.to_i + 5, default_settings: { default_payment_method: customer_payment_method.id, }, phases: [ { plans: [ { plan: plan1.id, quantity: 1 }, ], prorate: false, default_tax_rates: [tax_rate], }, ], } ) puts '-' * 100 puts "Wait until subscription schedule starts" puts '-' * 100 until soon_start_subscription_schedule.status == 'active' do soon_start_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(soon_start_subscription_schedule.id) puts soon_start_subscription_schedule.status sleep 2 end started_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(id: soon_start_subscription_schedule.id, expand: ['subscription']) future_subscription_schedule = Stripe::SubscriptionSchedule.create( { customer: customer.id, start_date: Time.now.to_i + 100*24*60*60, default_settings: { default_payment_method: customer_payment_method.id, }, phases: [ { plans: [ { plan: plan1.id, quantity: 1 }, ], prorate: false, default_tax_rates: [tax_rate], }, ], } ) # current_phase.start_date is not subscription cycle start_date def update_subscription_schedule(base_subscription_schedule) if base_subscription_schedule.status == 'active' start_date = base_subscription_schedule.current_phase.start_date elsif base_subscription_schedule.status == 'not_started' start_date = base_subscription_schedule.phases[0].start_date else raise end end_date = Time.at(start_date).since(3.months).to_i Stripe::SubscriptionSchedule.update( base_subscription_schedule.id, { # Set "cancel" not "releasse" end_behavior: 'cancel', phases: [ { plans: [ { plan: base_subscription_schedule.phases[0].plans[0].plan, quantity: base_subscription_schedule.phases[0].plans[0].quantity }, ], # prorate does not effect to Subscription or SubscriptionSchedule Canceling # prorate: true / false, default_tax_rates: base_subscription_schedule.phases[0].default_tax_rates.map(&:id), # For Update You must set start_date in first phase not in subscription schedule directly start_date: start_date, # If you do not want to get upcoming invoice on Subscription # Then you must specify end_date with Subscription natural cycle interval end_date: end_date }, ] } ) end updated_started_subscription_schedule = update_subscription_schedule(started_subscription_schedule) updated_future_subscription_schedule = update_subscription_schedule(future_subscription_schedule) put_subscription_schedule(updated_started_subscription_schedule, 'UPDATED') put_subscription_schedule(updated_future_subscription_schedule, 'UPDATED') end # NOTE # SubscriptionSchedule current_phase is not same as Subscription cycle # it is "phase" start_date and end_date # # If phase end_date is 3.months after then ... # # Time.at(updated_started_subscription_schedule.current_phase.start_date) # => 2020-01-02 17:44:23 +0900 # Time.at(updated_started_subscription_schedule.current_phase.end_date) # => 2020-04-02 17:44:23 +0900Started Subscription - cancel at current phase
Upcoming invoice leaves from dashboard
Not Started Subscription - cancel at first phase
Theres Starts Ends in both in future
Theres upcoming invoice becauce Schedule will start in future
Not current invoice because Schedule not startedStarted Subscription - cancel at future phase
Change end_date in code and run Case
Not Started Subscription - cancel at not first phase
Change end_date in code and run Case
Result
- This is run code resutls example but not correctly same as Image capture images in this article.
---------------------------------------------------------------------------------------------------- Wait until subscription schedule starts ---------------------------------------------------------------------------------------------------- not_started not_started not_started not_started not_started not_started active ---------------------------------------------------------------------------------------------------- Subscription Schedule CREATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955005, "current_phase": null, "customer": "cus_GTM5yQ0vRozYUW", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE", "default_source": null, "invoice_settings": null }, "end_behavior": "release", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNHCmti5jpytUA4yh9gmW", "object": "tax_rate", "active": true, "created": 1577954987, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1589187005, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5XKzi7c1qy8", "quantity": 1, "tax_rates": [ ] } ], "prorate": false, "start_date": 1586595005, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPNZCmti5jpytUWUBqK4lG", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNJCmti5jpytU8hFkMssH", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577954989, "current_phase": { "end_date": 1585817393, "start_date": 1577954993 }, "customer": "cus_GTM5yQ0vRozYUW", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNHCmti5jpytUA4yh9gmW", "object": "tax_rate", "active": true, "created": 1577954987, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1585817393, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5XKzi7c1qy8", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1577954993, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPNZCmti5jpytUoCQuoPaf", "status": "active", "subscription": "sub_GTM59xT4BS5E3j" } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNJCmti5jpytU8hFkMssH ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955005, "current_phase": null, "customer": "cus_GTM5yQ0vRozYUW", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNHCmti5jpytUA4yh9gmW", "object": "tax_rate", "active": true, "created": 1577954987, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1594457405, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5XKzi7c1qy8", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1586595005, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPNaCmti5jpytUnlvLBTke", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN ---------------------------------------------------------------------------------------------------- Wait until subscription schedule starts ---------------------------------------------------------------------------------------------------- not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started not_started active ---------------------------------------------------------------------------------------------------- Subscription Schedule CREATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955067, "current_phase": null, "customer": "cus_GTM5Wlau0N9HHj", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk", "default_source": null, "invoice_settings": null }, "end_behavior": "release", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNbCmti5jpytUOMYY1jCE", "object": "tax_rate", "active": true, "created": 1577955007, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1589187067, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5akDaPHCKOr", "quantity": 1, "tax_rates": [ ] } ], "prorate": false, "start_date": 1586595067, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPOZCmti5jpytUsHM0JqYJ", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5a ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPNdCmti5jpytUnvii1fUR", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955009, "current_phase": { "end_date": 1585817414, "start_date": 1577955014 }, "customer": "cus_GTM5Wlau0N9HHj", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNbCmti5jpytUOMYY1jCE", "object": "tax_rate", "active": true, "created": 1577955007, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1585817414, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5akDaPHCKOr", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1577955014, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPOaCmti5jpytU2OqMiUUC", "status": "active", "subscription": "sub_GTM64npF4L1rop" } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNdCmti5jpytUnvii1fUR ---------------------------------------------------------------------------------------------------- Subscription Schedule UPDATED ---------------------------------------------------------------------------------------------------- { "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a", "object": "subscription_schedule", "canceled_at": null, "completed_at": null, "created": 1577955067, "current_phase": null, "customer": "cus_GTM5Wlau0N9HHj", "default_settings": { "billing_thresholds": null, "collection_method": "charge_automatically", "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk", "default_source": null, "invoice_settings": { "days_until_due": null } }, "end_behavior": "cancel", "livemode": false, "metadata": { }, "phases": [ { "application_fee_percent": null, "billing_thresholds": null, "collection_method": null, "coupon": null, "default_payment_method": null, "default_tax_rates": [ { "id": "txr_1FwPNbCmti5jpytUOMYY1jCE", "object": "tax_rate", "active": true, "created": 1577955007, "description": null, "display_name": "Tax Rate", "inclusive": false, "jurisdiction": null, "livemode": false, "metadata": { }, "percentage": 10.0 } ], "end_date": 1594457467, "invoice_settings": null, "plans": [ { "billing_thresholds": null, "plan": "plan_GTM5akDaPHCKOr", "quantity": 1, "tax_rates": [ ] } ], "prorate": true, "start_date": 1586595067, "tax_percent": 10.0, "trial_end": null } ], "released_at": null, "released_subscription": null, "renewal_interval": null, "revision": "sub_sched_rev_1FwPObCmti5jpytU5eKvVg9W", "status": "not_started", "subscription": null } ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5aOriginal by Github issue
- 投稿日:2020-01-03T18:11:16+09:00
railsのDBで型をintegerからfloatへ変更しnumber_fieldで小数点を扱えるようにする
前回の記事の続きで初めてfloat型を扱ったのでメモ
やりたい事
- DBに間違ってinteger型で作ったカラムをfloat型に変更
- form_for内でnumber_fieldで小数点を扱えるようにする
- DBに保存した値を合計するメソッドを作る
参考
【Rails】form_for の number_field で小数を入力できるようにする
https://qiita.com/tegnike/items/07f789eb22c7a7bf6a19マイグレーションを使ったカラムの追加、削除、データ型の変更 [ 自分用メモ ]
https://qiita.com/dawn_628/items/13fa64dc6d600e921ce3【Rails】カラムの合計値を求める!
https://qiita.com/tomokichi_ruby/items/8758a91566957cfc5429number_fieldでは整数のみしか扱えない
new.html.erb<%= form_for @count_time do |f| %> <%= f.number_field :count_hour, class:'input_form' %> <%= f.submit '登録', class:'input_submit' %> <% end %>というフォームがあって,
new.html.erb<%= form_for @count_time do |f| %> <%= f.number_field :count_hour, step: '0.1', class:'input_form' %> <%# step: '0.1'を追加 %> <%= f.submit '登録', class:'input_submit' %> <% end %>
step: '0.1'を追加すると小数点以下第一位までの数値を扱えるようになりますこのままだとDBには整数でしか保存されない
DBの
count_hourはinteger型なので整数のみしか保存してくれないのでカラムの型を変更するターミナル$ rails g migration change_data_count_hour_to_count_time Running via Spring preloader in process 94544 invoke active_record create db/migrate/20200103074431_change_data_count_hour_to_count_time.rbカラム名がcount_hourでテーブル名がcount_timeになります
20200103074431_change_data_count_hour_to_count_time.rbclass ChangeDataCountHourToCountTime < ActiveRecord::Migration[5.2] def change change_column :count_times, :count_hour, :float #追記。テーブル名は複数形で end end
rails db:migrateしますこれでfloat型に変更できたのでnumber_fieldから送られる小数点の値がDBへ保存できるようになりました
DBの値をビューへ描画
index.html.erb<table> <tr> <th>日付</th> <th>時間</th> </tr> <% @count_time.each do |count_time| %> <tr> <td><%= count_time.created_at.to_date %></td> <td><%= count_time.count_hour %>時間</td> </tr> <% end %> </table>sumメソッドを使い学習時間の合計を表示させる
count_time_controller.rbclass CountTimesController < ApplicationController def index @count_time = CountTime.where(user_id: current_user.id).order('updated_at DESC').limit(5) @count_hour = @count_time.sum(:count_hour) end endsumメソッドの引数にカラムを指定するで合計値を出します
完成
まとめ
ひとまずこれで自分が最初に想像していた主要機能が完成しました。開発にそんな時間がかからないアプリでしたが知らない事も学べたので良かったかなと。あとは思い付いた追加の機能を開発していこうと思います。
終わり
- 投稿日:2020-01-03T17:18:28+09:00
コントローラー名とアクション名でSCSSを呼び出す
例えば WelcomeController の index アクションが呼ばれた場合、デフォルトの状態だとビューには welcome.css(.scss) がロードされるのだけど、これを呼ぶかどうかについて、ファイルの有無とか controller での変数で判定できたらいいのになという話。
色々方法はあるかと思うのですが、以下のようにしてみました。
assets/stylesheets/application.css
= require_self
*= require_directory .
デフォルトだと require_tree . となっている箇所を require_directory . にする。こうすることでルートディレクトリ( assets/stylsheets/ )の css しか自動でインポートしないようにする。ついでにスタイルシートのディレクトリも以下のように views っぽくする。
f
ltcmdr927:20121209105229p:plain
HEAD タグは application.html.erb で編集しているので、そこを修正する。
views/layouts/shared/application.html.erb
<%= stylesheet_link_tag "application", :media => "all" %>
<%= partial_stylesheet_link_tag controller.controller_name, controller.action_name, @partial_css_disabled %>
<%= javascript_include_tag "application" %>
stylesheet_link_tag と javascript_include_tag の間に新しい helper メソッドを追加する。引数でビューを読んだ時のコントローラ名とアクション名を指定。@partial_css_disabled は後述。app/helpers/application_helper.rb
module ApplicationHelper
def partial_stylesheet_link_tag(controller_name, action_name, disabled = true)
if disabled == false || disabled.nil?
if File.exist?("#{Rails.root.to_s}/app/assets/stylesheets/#{controller_name}/#{action_name}.css.scss")
return stylesheet_link_tag "#{controller_name}/#{action_name}"
end
end
endend
partial_stylesheet_link_tag メソッドを書く。disabled が false かあるいは nil かを判定し、正なら scss ファイルの有無を判定し、最終的にコントローラ/アクションの stylesheet_link_tag を戻す、という感じ。基本的にはファイルの有無で stylesheet タグが追加されるかどうかを判定するようになるので、例えばファイルは存在するけどタグは追加したくないという場合は、下記のように controller で @partial_css_disabled 変数に true を指定してやる。 true を指定するとファイルが存在していてもタグが追加されない。
app/controller/welcome_controller.rb
class WelcomeController < ApplicationController
def index
@partial_css_disabled = true
endend
若干イモっぽい感じがしなくもないですが、個人的にはこれで管理しやすくなりました。(コントローラの階層構造が深くなった場合については調査して後々追記予定。)
- 投稿日:2020-01-03T17:01:46+09:00
Railsチュートリアルメモ - 第4章・第5章
Railsチュートリアル第4章はこちら
Railsチュートリアル第5章はこちらRailsチュートリアルで気づいたことのメモを記載していきます
4.1 動機
Rubyに関する知識を深めていく章。
手始めに条件分岐で異なる文字列を返すhelperを自作する。4.2 文字列とメソッド
ポイント
- 文字列の式展開 => Rubyはシングルクォート文字列の中では式展開を行わない(シェルスクリプトと同じ)
- putsメソッド(printメソッド + 行末の改行)
- nil
- 暗黙の戻り値 => メソッド内にreturnを明示しなくても最後に評価した式をreturnする
- mixed in => moduleをincludeすること
- Railsでは自動的にヘルパーモジュールをmixed inしてくれるため、include行を書かなくてもOK
4.3 他のデータ構造
たぶんここらへんで眠くなって落脱する人が多い気がする、、、
初心者は流し読みにして、後でわからないところが出てきたら戻ってくるくらいの方が良さそうポイント
- Rubyの配列はゼロオリジン(ゼロ・インデックス)
- 比較演算子(==、!=)
- 破壊的メソッド(メソッドの最後が!)
- pushメソッド (または同等の<<演算子)
- 範囲 (range) e.g (0..9).to_a
Rubyは改行と空白を区別しない(式の途中で改行してもOK)
4.4 Rubyにおけるクラス
ポイント
- リテラルコンストラクタ、名前付きコンストラクタ
- superclassメソッド
- selfキーワード
- eachメソッド
- ブロック e.g. { |i| puts 2 * i } => pythonでいうlambda?
- ハッシュとシンボル
- 以下は等価
- { :name => "Michael Hartl" }
- { name: "Michael Hartl" }
- 組み込みクラスの変更が可能
- attr_accessor => attributeに対しるaccessorを作成するための記法
- initialize => 初期化メソッド(pythonでいうinitと同じ)
4.5 最後に
割愛
5.1 構造を追加する
html/cssについての解説の章。
ポイント
- パーシャル(DOMの一部を_始まりのファイルに切り出せる)
5.2 Sassとアセットパイプライン
SASS(CSSプリプロセッサー)の説明とRailsでの指定の仕方についての章。
ポイント
- アセットディレクトリ(*/assetsで表される静的ファイルを目的別に分類するディレクトリ)
- マニフェストファイル(アセットディレクトリにあるファイルをどのようにまとめるかを定義するファイル。実際の処理はSprocketsというgemが行う)
- プリプロセッサエンジン(.scssや.erbなどを変換して、html/css/jsに置き換えてくれるやつ。Sprocketsがアセットをまとめてくれた後にプリプロセッサエンジンが実行される)
- Sassの変数
- $始まりで変数を定義できる
- bootstrap lessで定義されている変数(@gray-light)なども$gray-lightと書けば使用可能
5.3 レイアウトのリンク
- 名前付きルート
- config/routes.rbの中で
get '/help', to: 'static_pages#help', as: 'helf'のように書くとhelp_pathやhelp_urlのような形で呼び出せるようになる- 統合テストの作成
rails generate integration_test site_layoutで統合テストを作成できるrails test:integrationで統合テストの実行5.4 ユーザー登録: 最初のステップ
6章以降でユーザー登録処理を作成していくための導入。ここではこれまでの章の振り返り的な内容。
5.5 最後に
割愛
- 投稿日:2020-01-03T13:03:11+09:00
rails: belongs_to :user, optional: trueとは?
簡単に説明すると、belongs_toで
optional: trueを設定することにより外部キーのnilを許可できるようになります。
optional: trueはどんなときに使われるのか?例えば、Q&Aのようなサービスを作る際にuserテーブルとquestionテーブルがあるとする。
その際に、ログインしていないユーザーからでも質問をポストできるシステムを作るときに使われたりする。
基本的にアソシエーションを書くときは外部キーが無いっていうことはあまりないがたまにある時もあるのでその際にoptional: trueをつけなかったりするとバグの温床になるので気をつける必要がある。ただDBの設計上、外部キーのnilを許可することが少ない気がするので、あまり使う設定ではないかもしれません。
- 投稿日:2020-01-03T12:57:29+09:00
pickadate.jsを使ってフォームにカレンダーを表示させる
こんにちは!スージーです!
久し振りにrailsでアプリ開発を開始したのでその際に使ったdatepickerについてまとめdatepickerとは
日付入力フォームを選択した時に表れるカレンダー
参考記事
結局、どのdatepickerが一番使い勝手がよいのかhttps://qiita.com/knt45/items/6d74f6785cd4547ae53b
Rails Application Build Guides
https://rails.densan-labs.net/form/datetime_register_form.htmlpickadate.js
https://amsul.ca/pickadate.js/pickadate-rails
https://github.com/veracross/pickadate-rails準備
Gemfilegem 'pickadate-rails'で
bundle installapplication.js//= require pickadate/picker 追記 //= require pickadate/picker.date 追記 //= require pickadate/picker.time 追記 //= require jquery_ujs //= require_tree .application.scss@import "pickadate/classic"; @import "pickadate/classic.date"; @import "pickadate/classic.time";cssレイアウトは
default又はclassicが選べるようになっているJsファイル
datepicker.js$(function() { $( "#datepicker" ).pickadate(); // カレンダー表示のイベント });Jsはこれだけで使えるようになる。リファレンスには色々なレイアウトのカレンダーが用意されている。
turbokinksでイベント発火しない
jQueryあるあるですがturbolinksをtrueのままにしているとページをリロードしないとイベント発火しないので、今回は不要なので削除。
application.js,application.html.erbのturbolinksに関するコードは削除。gem 'turbolinks'も削除してbundle installします。contorollerのアクション作成
datepickers_controller.rbclass datepickersController < ApplicationController def new @datepicker = Datepicker.new end def create @datepicker = Datepicker.create(datepicker_params) if @datepicker.save redirect_to :root else render :new end end private def datepicker_params params.require(:start_day).permit(:start_day).merge(user_id: current_user.id) end endこの辺はお決まりのnew/createアクションです
完成
カレンダーで日付を選択して登録できている事をパラメータを確認
ターミナルStarted POST "/start_days" for ::1 at 2020-01-03 12:50:13 +0900 Processing by StartDaysController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"4Ug39AI0MM0azF2XqNEcTOSQNjpNWQ+ovCSI7PFrPPatvEM8avbMZ6MII5JdxZnni+Hssbo7x6NTAjA0udiH7Q==", "datepicker"=>{"datepicker"=>"3 January, 2020"}, "commit"=>"登録"}パラメータの値がちゃんとDBに保存されています
まとめ
日付の選択は何かを登録する場面で使う事が多いかと思いますし、年月日をそれぞれ入力するのはUI/UX的に煩雑になってしまいますが、datepickerを使えば便利になりますし、簡単に実装もできました。
- 投稿日:2020-01-03T12:56:04+09:00
Stripe API / 例えば1ヶ月単位のサブスクリプションのキャンセルを予約した時に、サイクル請求の期間に対して中途半端な終了日を指定すると、次回のインヴォイスが日割計算されることが分かる。 ( #Ruby #Stripe )
Points
prorate: true
prorate: falseどちらでも挙動は変わらないように思えた
Code
#! /usr/bin/env ruby require 'stripe' # You Need # `$ gem install activesupport` befure run ruby script require 'active_support/core_ext' # Docs # https://stripe.com/docs/api/subscriptions/create # https://stripe.com/docs/billing/subscriptions/billing-cycle#prorations Stripe::api_key = ENV['STRIPE_SECRET_KEY'] def create_subscription_and_cancel(cancel_at_natural_cycle_end:, prorate:) product1 = Stripe::Product.create(name: "Gold plan #{rand(9999999999)}") plan1 = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 980, product: product1.id, usage_type: 'licensed') tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false) customer = Stripe::Customer.create payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01}) customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id) subscription = Stripe::Subscription.create( { customer: customer.id, default_payment_method: customer_payment_method.id, items: [ [ { plan: plan1.id }, ], ], prorate: prorate, default_tax_rates: [tax_rate], } ) cancel_at = if cancel_at_natural_cycle_end Time.at(subscription.current_period_start).since(1.month).to_i else Time.at(subscription.current_period_start).since(1.month).ago(1.day).to_i end updated_subscription = Stripe::Subscription.update( subscription.id, cancel_at: cancel_at ) puts '-' * 100 puts "SUBSCRIPTION" puts '-' * 100 puts "prorate: #{prorate}" puts "cancel_at: #{updated_subscription.cancel_at} ( #{Time.at(updated_subscription.cancel_at)} ) " if ENV['VERBOSE'] puts '-' * 100 puts updated_subscription end puts '-' * 100 puts 'UPCOMING INVOICE' puts '-' * 100 begin upcoming_invoice = Stripe::Invoice.upcoming(customer: updated_subscription.customer) puts "subotal: #{upcoming_invoice.subtotal}" puts "tax: #{upcoming_invoice.tax}" puts "total: #{upcoming_invoice.total}" rescue Stripe::InvalidRequestError => e puts e.message end if ENV['VERBOSE'] puts '-' * 100 puts upcoming_invoice end puts '-' * 100 puts "https://dashboard.stripe.com/test/subscriptions/#{subscription.id}" updated_subscription end create_subscription_and_cancel(cancel_at_natural_cycle_end: true, prorate: true) create_subscription_and_cancel(cancel_at_natural_cycle_end: false, prorate: true) create_subscription_and_cancel(cancel_at_natural_cycle_end: true, prorate: false) create_subscription_and_cancel(cancel_at_natural_cycle_end: false, prorate: false)Result
---------------------------------------------------------------------------------------------------- SUBSCRIPTION ---------------------------------------------------------------------------------------------------- prorate: true cancel_at: 1580610835 ( 2020-02-02 11:33:55 +0900 ) ---------------------------------------------------------------------------------------------------- UPCOMING INVOICE ---------------------------------------------------------------------------------------------------- No upcoming invoices for customer: cus_GTG1yiBZ6EXEEh ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscriptions/sub_GTG1xUoFpeHjYG ---------------------------------------------------------------------------------------------------- SUBSCRIPTION ---------------------------------------------------------------------------------------------------- prorate: true cancel_at: 1580524440 ( 2020-02-01 11:34:00 +0900 ) ---------------------------------------------------------------------------------------------------- UPCOMING INVOICE ---------------------------------------------------------------------------------------------------- subotal: -32 tax: -3 total: -35 ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscriptions/sub_GTG1TVlYdpyHgl ---------------------------------------------------------------------------------------------------- SUBSCRIPTION ---------------------------------------------------------------------------------------------------- prorate: false cancel_at: 1580610845 ( 2020-02-02 11:34:05 +0900 ) ---------------------------------------------------------------------------------------------------- UPCOMING INVOICE ---------------------------------------------------------------------------------------------------- No upcoming invoices for customer: cus_GTG1UAKVPjh6uR ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscriptions/sub_GTG1ZiZpe6qBoW ---------------------------------------------------------------------------------------------------- SUBSCRIPTION ---------------------------------------------------------------------------------------------------- prorate: false cancel_at: 1580524452 ( 2020-02-01 11:34:12 +0900 ) ---------------------------------------------------------------------------------------------------- UPCOMING INVOICE ---------------------------------------------------------------------------------------------------- subotal: -32 tax: -3 total: -35 ---------------------------------------------------------------------------------------------------- https://dashboard.stripe.com/test/subscriptions/sub_GTG1KIKsD5Q2qbA
B
C
D
Original by Github issue
- 投稿日:2020-01-03T12:14:17+09:00
初学者によるプログラミングMemo #16 sliceメソッド([]メソッド)
はじめに
今回はsliceメソッドを使用するお話です
破壊的メソッドにも触れます
なお、本記述はMacにおいて、Railsでの開発を前提としています
また、まだまだひよっこですので、不備等ございましたらご指摘いただけると幸いです目次
- "slice"メソッドについて
- []について
- 破壊的メソッドについて
- 問題
- 破壊的メソッドを使わない様に書いてみる
"slice"メソッドについて
こちらによると「sliceメソッドは、[]の別名です。引数にしたがって文字列の中から部分文字列を取り出します。」とあります
引数に整数一つを指定すると、その一文字を取り出す事ができます *Ruby1.9以降
少し使ってみますarray = "school" p array.slice(0) => "s"0番目の一文字を取り出せました
元の変数には変化がありませんarray = "school" p array.slice(0) p array => "s" => "school"[]について
silceメソッドの別名というだけあって、使い方は同じです
array = "school" p array[0] => "s"0番目の一文字を取り出せました
同じく元の変数には変化がありませんarray = "school" p array[0] p array => "s" => "school"記述的にはシンプルなので、こちらを使うほうが良さそうですね
破壊的メソッドについて
破壊的メソッドとは、実行した時に元となったのオブジェクト自身の内容を変更してしまうメソッドのことです。
基本的には、メソッドの最後に"!"がつきます
先ほどの配列"array"でやってみましょうarray = "school" p array.slice!(0) => "s"ここまでは同じです
しかし、元の配列の中身を示すとこうなりますarray = "school" p array.slice!(0) p array => "s" => "chool""s"が取り出されたことにより、元の配列から"s"が消えてしまいました
この様に、元のオブジェクトの値自体を変更してしまうメソッドのことを破壊的メソッドと呼びます問題
私の通っているスクールで、この様な問題が出ました
同じだと問題があるので、少し変えますarray('Ruby') => 'byRu' array('python') => 'thonpy' array('Hi') => 'Hi'配列"array"に対して、「最初から2文字を最後尾に持ってくるメソッドを作ろう」という問題です
先ほどの"slice"メソッドと、破壊的メソッドを使用すれば、簡単に答えを導く事ができます
前提知識として、slice(0,2)とすれば、「前から数て"0"番目から"2"文字分を取り出す」という意味になることが必要ですdef array(str) array = str.slice!(0,2) new_array = str + array return new_array end p "文字を入力してください" input = gets.chomp p array(input)"input"のなかに好きな文字を入れれば最初から2文字を最後尾に持ってくる事が可能です
しかし、問題は破壊的メソッドを使用しているという点です
破壊的メソッドに関して、何名かのご意見を拝見いたしましたが、初学者の私にとってどうすべきかの判断は付けづらかったので、状況によっては使って良いという解釈で行きたいと思います。
使わないほうがいいと思った理由の一つには、元のオブジェクトの中身を変えてしまうので、自分以外の人がメンテナンスをする際に辿るのが大変な事があげられます。
使ってもいいシチュエーションがあるなと感じた理由の一つには、配列オブジェクトに対してハッシュオブジェクトを格納していく際、"<<"メソッドを使用します(これも破壊的メソッドです)が、そういった場合は使ってもいいのではないかなあと思います。今の私では、それ以外で配列にハッシュオブジェクトを格納する方法を知らないということもありますが…破壊的メソッドを使わない様に書いてみる
では、先ほどの問題を、破壊的メソッドを使わずに解いてみましょう
といっても、最初から2文字を取り出せているので、そこまで難しくはありません。
前提知識として、「数値をマイナスにすると後ろから数えて何番目を指定する」ということくらいですdef array(str) array = str.slice(0,2) length = str.length back = str.slice(2-length,length-2) new_array = back + array return new_array end p "文字を入力してください" input = gets.chomp p array(input)こんな感じですね
ポイントは"(2-length,length-2)"のところでしょうか
入力される文字列が何文字か決まっていないので、"2-length"とすることで、後ろから3番目を必ず指定できます
今回は最初から2文字と決まっているので、2から引きましたが、これも何文字までと指定する様に変えてあげれば汎用性(あるのか?)が増えますね今回はこれで以上です
- 投稿日:2020-01-03T11:33:09+09:00
[Ruby] EnumerableとEnumeratorとEnumerator.produce
はじめに
Enumerator.produceは、Ruby 2.7.0 で新たに追加されたメソッドです。- 本記事では
Enumerator.produceを紹介したいんですが、便宜上Enumeratorとは、そもそもEnumerableとは一体まで遡ってまとめていますので、そんなことはいいからproduceメソッドを教えろという方は下の方までスクロールお願いします。Enumerable
- Enumerableはモジュール
- 標準クラスだと
ArrayとかHashなんかがインクルードしている (他にもたくさん)- インクルードする場合、
eachメソッドの実装が必須eachメソッドを用いて、all?collectfirstmaxなど、様々なメソッドを提供するもちろん自作のクラスをEnumerable化することもできます。
class MyClass include Enumerable def initialize(a, b, c, d) @a = a @b = b @c = c @d = d end def each yield @a yield @b yield @c yield @d end end上記クラスは、4つの値
abcdを受け取って、そのままの順番のシーケンスを定義したeachメソッドを持ちます。実用性は皆無ですが、
Enumerableをincludeしていて、eachメソッドが定義されているので、ArrayやHashと同様に、Enumerableの恩恵を受けることができます。irb(main):030:0> MyClass.new(1,2,3,4).to_a => [1, 2, 3, 4] irb(main):031:0> MyClass.new(20,30,10,0).max => 30 irb(main):032:0> MyClass.new(1,2,3,4).filter(&:even?) => [2, 4]もちろん定義した
eachメソッドをそのまま使うこともできます。irb(main):033:0> MyClass.new(1,2,3,4).each { |n| p n } 1 2 3 4 => 4Enumerator
- Enumeratorはクラス
Enumerableをインクルードしているeach以外のメソッドでもEnumerableの機能を利用できるようにするためのラッパークラスとして使用前述の
MyClassでは、abcdの正順のシーケンスをeachメソッドに定義して、それを使ってEnumerableの機能を扱えるようにしました。それはそれとして、
dcbaの逆順のシーケンスを使って、同様にEnumerableの機能を使いたくなったとします。そこで今回は、以下のように
Enumeratorオブジェクトを戻す、each_reverseメソッドを定義します。class MyClass include Enumerable def initialize(a, b, c, d) @a = a @b = b @c = c @d = d end def each yield @a yield @b yield @c yield @d end def each_reverse Enumerator.new do |y| y << @d y << @c y << @b y << @a end end end
Enumeratorオブジェクトは、オブジェクト生成時の定義を元にしたeachメソッドを持っているため、dcba順のシーケンスに対してもEnumerableの機能が使えるようになります。irb(main):020:0> MyClass.new(1,2,3,4).each_reverse => #<Enumerator: #<Enumerator::Generator:0x000055b74e71a380>:each> irb(main):021:0> MyClass.new(1,2,3,4).each_reverse.first => 4 irb(main):022:0> MyClass.new(1,2,3,4).each_reverse.map { |n| n ** 2 } => [16, 9, 4, 1]ちなみに
ArrayやHashのeachメソッドはEnumeratorを返します。irb(main):041:0> [1,2,3,4,5].each => #<Enumerator: [1, 2, 3, 4, 5]:each> irb(main):042:0> {hoge: 'Hoge'}.each => #<Enumerator: {:hoge=>"Hoge"}:each>Enumerator.produce
- Enumerator.produce はクラスメソッド
- ブロックで定義した無限リストを簡易的に生成できる
Enumerator.produceは、初期値とブロックを定義することで無限リストになるEnumeratorオブジェクトを返します。以下は、初期値が0で、1ずつ加算するだけの単純な無限リストです。Enumerable#takeを使って、先頭の10件を取得していますが、10件分だけ無限リストを評価するようになっています。irb(main):007:0> enumerator = Enumerator.produce(0) { |n| n + 1 } irb(main):008:0> enumerator.take(10) => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]無限リストになるので、もちろん
to_aとかやると無限にリストを走査し続けるので死にます。irb(main):027:0> enumerator = Enumerator.produce(0) { |n| n + 1 } irb(main):028:0> enumerator.to_a # ゆるやかな死以下のように、日付の無限リストを作ることもできるので実用性も充分です。
require 'date' enum = Enumerator.produce(Date.today, &:next_day) pp enum.take(10)[#<Date: 2020-01-02 ((2458851j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-03 ((2458852j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-04 ((2458853j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-05 ((2458854j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-06 ((2458855j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-07 ((2458856j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-08 ((2458857j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-09 ((2458858j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-10 ((2458859j,0s,0n),+0s,2299161j)>, #<Date: 2020-01-11 ((2458860j,0s,0n),+0s,2299161j)>]以下の例はフィボナッチ数列の無限リストを生成するコードです。(参考)
現在の値と直前の値があれば次の値を計算できるので、実に
Enumerator.produceに向いていて美しいですね。Enumerator.produce([0, 1]) { |prev, current| [current, prev + current] }.take(10).map(&:first) => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
- 投稿日:2020-01-03T11:28:18+09:00
anyenv で rbenv を使っているときの最新版 Ruby のインストール
anyenv で rbenv を入れていて、通常の
brew upgrade rbenv ruby-buildをやっても最新版の Ruby が表示されなかったのでメモ。anyenv を使っていると以下のコマンドで env 系を一括でアップデートできる。
anyenv update上記コマンド実行後、
rbenv install -lを実行すると最新版の Ruby が表示された。
- 投稿日:2020-01-03T00:28:25+09:00
WSLでRails6の環境構築
VSCode上で環境構築します。
WSLを有効にし、Ubuntuをインストール
こちらの記事通りに進めていけば簡単にできます。画像付きですごく分かりやすかったです。
UbuntuはLTS版を入れました。VSCodeの設定
VSCodeの拡張機能
Remote Developmentをインストール。これでWSLに接続して開発ができるようになる。
インストールしたら、VSCodeの一番左下に緑のアイコンがでるのでクリック。
Remote-WSL: New Windowを選択。これで設定終了。Ubuntuのパッケージを最新化
VScodeのターミナルを開いて、2つのコマンドを実行。かなり時間かかりました。
sudoコマンドを使うときに、Ubuntuで設定したパスワードを聞かれるかも。パッケージの最新化$ sudo apt update $ sudo apt upgrade -yrbenvのインストール
rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使える。バージョンを変更したい場合も簡単。
参考: rbenvrbenvのインストールと設定#rbenvをインストール $ git clone https://github.com/rbenv/rbenv.git ~/.rbenv #パス指定と初期化処理を.bashrcに追加 $ echo 'export PATH="$HOME"/.rbenv/bin:$PATH' >> ~/.bashrc $ echo 'eval "$(rbenv init -)"' >> ~/.bashrc #パス指定と初期化処理の反映 $ source ~/.bashrc #バージョンが表示されれば成功 $ rbenv -vruby-buildのインストール
ruby-buildはRubyのインストールを簡単にしてくれる。
参考: ruby-build、ruby-buildのWikiruby-buildのインストール、その他必要パッケージ# ruby-buildのインストール $ mkdir -p "$(rbenv root)"/plugins ←これいるの?やらなくてもできた。 $ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build #Rubyのインストールに必要なパッケージ。ruby-buildのWiki参照。かなり時間かかる。 $ sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-devRubyのインストール
ようやくrbenvを使ってRubyのインストールをします。安定版をインストールする。
恐ろしいくらい時間がかかりました。
最新の安定版の確認: こちらRubyのインストール#Rubyのインストール $ rbenv install 2.7.0 #PC全体で使うRubyのバージョンを指定する $ rbenv global 2.7.0 $ ruby -vRubyGemsのアップデート
RubyGemsを最新版にアップデートする。
$ gem update --systemRailsのインストール
Railsをインストールします。
最新のバージョンの確認: こちら$ gem install rails -v 6.0.2.1 $ rails -vNode.jsのインストール
Rails6から標準のWebpackerのインストールに必要。
# npmのインストール $ sudo apt install -y npm # Node.jsのインストール $ sudo npm install n -g $ sudo n stable $ node -vyarnのインストール
Webpackerのインストールに必要。
最初この方法でやったがうまくできなかった。
参考: yarn$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - $ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list $ sudo apt-get update && sudo apt-get install yarn $ yarn -v #エラー出た時のメモ $ sudo apt remove cmdtest $ sudo apt remove yarn $ sudo apt autoremoveSQLite3のインストール
Rails標準のデータベースなのでインストール。
$ sudo apt-get install libsqlite3-devアプリの作成
$ rails new sample_app $ cd sample_app $ rails sその他メモ
railsインストールするときノードキュメント
# gemでライブラリーをインストールした際にドキュメントを生成すると時間がかかる。 # そこで、ドキュメントを生成しない設定にする $ cd $ touch .gemrc $ echo "install: --no-document" >> .gemrc $ echo "update: --no-document" >> .gemrc # rbenvのアップデート # Rubyの最新バージョンを使いたいときは、rbenvをアップデートしてから導入 $ cd ~/.rbenv $ git pull $ cd plugins/ruby-build $ git pull # 新しいコマンドを要するgemをインストールしたとき使用 $ rbenv rehash # 新しく作ったディレクトリで作業するときは、そこで使うRubyのバージョンを指定する $ rbenv local バージョン感想
ほとんどエラー無くできたが、各種パッケージのインストールが長すぎて疲れました笑
それにすごい動作が遅い
- 投稿日:2020-01-03T00:01:44+09:00
Ruby on Rails を学ぶ _No.1
view(ビュー)
ページや「見た目」を作るためのHTMLファイルです。
ブラウザとrailsの通信でrailsからビューが返され、ページが表示されます。controller(コントローラー)
ページを表示するときにrailsの中ではコントローラを経由してビューをブラウザに返します。
具体的には、コントローラと同じ名前のビューフォルダから、あくしょんと同じ名前のHTMLファイルを探してブラウザに返します。■ コントローラファイルの中身
ファイルの中にtopメソッドが追加されます。コントローラの中のメソッドはアクションと呼ばれます。
→ アクションと同じ名前のHTMLファイルをブラウザに返します。html_controller.rbclass HomeController < ApplicationController def top #homeコントローラのtopアクションに対応するhtmlファイルを返す end endルーティング
ブラウザとコントローラを繋ぐ役割があります。
送信されたURLに対してどのコントローラのどのアクションで対応するかを決める対応表のような物です。■ ルーティングファイルの中身
ルーティングはconfig/routes.rb内に定義され以下のような文法で書かれます。routes.rbRails.application.routes.draw do get "home/top" => "home#top" #get "URL名" => コントローラ名#アクション名 end


















