- 投稿日:2020-12-08T23:26:06+09:00
WindowsでWSL2(Ubuntu)にRailsを入れてgitコマンドでgithubにpushするまで
背景
WindowsのWSL2上でRails2.7.2を動かし、ブラウザでRailsが動いていることを確認し、コンソールからgitコマンドでgithubのmasterブランチにコードをpushするまでの手順です。
また、gitでのブランチの切り分けとプルリク方法、Railsのフォルダ構成を最後に載せています。
WSL 2(Windows Subsystem for Linux 2)を使えるようにする
① コントロールパネルから「プログラム」>「プログラムと機能」>「Windows の機能の有効化または無効化」を選択する。
表示されたチェックボックスにて「Linux 用 Windows サブシステム」「仮想マシンプラットフォーム」の二つにチェックを付ける。その後再起動を要求されるので再起動する。② Windows storeからubuntuをインストールする。
③下記コマンドでWSLのバージョンを確認する。
$ wsl -l -v NAME STATE VERSION * Ubuntu Running 1バージョンが1なので下記コマンドでバージョン2にする。
$ wsl --set-version Ubuntu 2
wsl --set-version Ubuntu 2
で「カーネルコンポーネントの更新が必要」という旨のエラーが出た場合、
https://docs.microsoft.com/ja-jp/windows/wsl/install-win10
上記リンクからwsl_update_x64.msi
をダウンロードしC:\Windows\System32\lxss\tools
に移動してから実行する。
wsl_update_x64.msi
の実行後に再度wsl --set-version Ubuntu 2
を実行する。※
wsl_update_x64.msi
はC:\Windows\System32\lxss\tools
に移動しないと実行時にエラーが出る。
※wsl_update_x64.msi
は上記リンクのx64 マシン用 WSL2 Linux カーネル更新プログラム パッケージ
からダウンロードできるが、ダウンロードがChromeにブロックされて進まない場合がある。その際はリンクを名前を付けて保存
し左下のダウンロード警告に対し継続
を選択すればダウンロードできる。RubyとRailsの環境構築
環境構築するにあたって知っておいたほうがいい事前知識は下記の記事にまとめました。
Ruby初心者が環境構築するため rbenv、RVM、bundler、gem 等の事前知識環境構築は下記の記事通りに実施します。
WSLで作るRuby on Rails環境構築 〜VSCode Remoteを添えて〜※上記記事の最終盤で
sudo apt install nodejs
した後にrails server
して「webpackerが必要」という旨のエラーが出た場合、webpackerをインストールする必要があります。
webpackerのインストールには最新のyarnが必要ですが、aptでyarnを入れても最新版でないためwebpackerを入れる際にエラーが出ます。なので下記の順番でwebpackerをインストールする必要があります。①下記を参考にyarnをインストールする。
yarnインストールの際にエラーがでた②webpackerをインストールする。
$ rails webpacker:install再びコマンドを実行し、Railsが動いていることをブラウザから確認できました。
$ rails serverコンソールからgitコマンドでgithubのmasterブランチにpushする
①gitに自分のemailとユーザー名を設定する。
※しないとコミット時にエラーが出ます。$ git config --global user.email メールアドレス $ git config --global user.name ユーザー名②下記の記事通り実施する。
【Rails入門】 Githubを導入する方法以上となります。
蛇足
下記の記事はGithubでプルリクする方法です。
初心者向けGithubへのPullRequest方法下記の記事はRailsのフォルダ構成です。
デフォルトのRailsフォルダ構造最後に、参考にさせていただいた記事の投稿者の皆様、ありがとうございます。
私の記事に不備などありましたらご指摘いただけると幸いです。
- 投稿日:2020-12-08T22:35:32+09:00
GCPの非同期プロダクトからのリクエストを認証する
こちらはVISITS advent calendar 14日目の記事です。
GCPのいくつかのプロダクトでは、処理の後に予め登録しておいたエンドポイントをHTTPで呼び出すことができます。
時限的に処理を開始したい場合や何かのイベントの後に特定の処理を行いたい場合などに、プロダクトと独立させて処理を実行できるため色々と融通が効きます。
受ける側もHTTPさえ受けられれば通常のWebアプリケーションでも問題ないので便利なのですが、リクエストを送ってきた相手が本当にGCPのプロダクトなのか確認する必要があります。
これについては、2019年4月頃よりサービスアカウントを用いたトークン認証(OAuth, OIDC)ができるようになったようです。
いくつか上記の認証を紹介する記事はあったのですが、具体的な認証の実装をしているものがあまり見当たらなかったので今回rubyで書いてみることにしました。
執筆にあたっては以下の記事を参考にさせていただきました。
この記事だけ読んで一通り設定できるようにしたいため、いくつか内容が重複するところあるかと思いますがご容赦ください。
- GCP からの HTTP リクエストをセキュアに認証する
- Automatic OIDC: Using Cloud Scheduler, Tasks, and PubSub to make authenticated calls to Cloud Run, Cloud Functions or your Server
GCPのHTTP認証
GCPでは以下のプロダクトについては、エンドポイントを登録しておくことで後処理をHTTPで投げられるようになっています。
- Cloud Scheduler
- Cloud Tasks
- Cloud Pub/Sub
上記のプロダクトからは以下のようなプロダクトに対して処理を投げることができます。
- Cloud Run
- Cloud Functions
- Cloud Endpoints
- その他任意のサーバー/サービス
GCPのプロダクトを組み合わせた場合、基本的に認証はGCP側でよしなにやってくれるため便利です。
またGCE/GAE/GKEといったHTTPを受けられるようなプロダクトや、GCP外の自前のサーバー等でも可能です。
ただし、この場合はエンドポイントを公開しているサーバー側で認証を対応する必要があります。認証の流れ
実際に認証する際は以下のような流れになります。
- 認証用のサービスアカウントを作成する
- 受信側で認証する
- 認証機構を持つプロダクトの場合:サービスアカウントにIAMで関連のロールを付与
- 自前の場合:送られてくるidトークンを認証する
- 送信側のプロダクトにサービスアカウントを紐付ける
1. 認証用のサービスアカウントを作成する
まずはサービスアカウントを作成します。
予めロールを設定するプロダクトが分かっていれば、それにあったプロダクトのロールをここで設定しますが、後ほど設定も可能なので後回しでも大丈夫です。今回は
gcp-oidc-auth@{project-id}.iam.gserviceaccount.com
のような名前にしました。2. 受信側で認証する
続いて受信側を設定します。
送信側の設定をする際に受信側のendpointを指定するので、先に受信側を用意しておく必要があります。2-1. 認証機構を持つプロダクトの場合
GCPプロダクトで認証できる場合は、さきほど作成したサービスアカウントに受信側プロダクトのロールを付与します。
今回は例としてCloud Runを取り上げます。なおCloud Endpointsに関しては、違った手順で認証を構成することになります。
Cloud Runサービスの作成
まずはCloud Runに飛んでサービスを作成します。
リージョンやサービス名は適当に決めて次へ。
コンテナを指定するところは、適当なイメージを選択します。
詳細設定内にあるサービスアカウントは、あくまでCloud Runが何かGCPのAPIを叩くときに使うサービスアカウントになります。
1で作成したものは送信側に設定するものなので、ここではCloud Run用(もっと言うとCloud Runのサービスごと)のサービスアカウントを割り当てた方が良いと思われます。3つ目にHTTPのトリガーを指定しますが、ここで「認証を必要とする」を選択して、Cloud Runサービスを作成します。
IAMの設定
最後の「認証を必要とする」では、IAMにて送信側に設定するサービスアカウントに適切なロールを付与する必要があります。
ロールは受信側プロダクトに依存したものを付与する必要があります。
- Cloud Run:
Cloud Run 起動元
- Cloud Functions:
Cloud Functions 起動元
2-2. 自前でIDトークンを認証する
自前でIDトークンを検証する場合は、トークンの中身について把握する必要があります。
サービスアカウントを紐付けると、そのGCPプロダクトからのリクエストのAuthorizationヘッダーにBearer Tokenがjwt形式で渡ってきます。
IDトークンをdecodeするとこのような形になります。
この場合1つ目のjsonがペイロード、2つ目がヘッダーになっています。[ { "aud": "https://hogehoge.com/path/to/endpoint", "azp": "11.................52", "email": "hoge-service-account@{project-id}.iam.gserviceaccount.com", "email_verified": true, "exp": 1606186661, "iat": 1606183061, "iss": "https://accounts.google.com", "sub": "11.................52" }, { "alg"=>"RS256", "kid"=>"dedc012d07f52aedfd5f97784e1bcbe23c19724d", "typ"=>"JWT" } ]ペイロードについては公式の説明がありますので、より詳しくはそちらを参照ください。
キー 内容 aud jwtのaudクレーム。 Cloud Scheduler
の場合デフォルトで受信側endpointのURLが入る。azp 独自のクレーム。認証された送信者のクライアントIDを指すらしいが、OAuthにおいてwebアプリとAndroidアプリなどで同じ人なのに違うIDで管理される場合などに使うらしい。今回は対象外か。 独自のクレーム。サービスアカウントが入ってくる。 email_verified ユーザー認証が済んでいればtrue。おそらくOAuthで一般ユーザーが送信する場合は認証済みでないケースは想定されるが、今回のサービスアカウントの場合は基本的にtrueのはず。 exp jwtのexpクレーム。ライブラリを使えば基本期限切れのチェックはやってくれる。 iat jwtのiatクレーム。 iss jwtのissクレーム。ID tokenの場合 https://accounts.google.com
かaccounts.google.com
のどちらかになる。sub jwtのsubクレーム。Googleのアカウント全体でアカウントを特定できる、ユニークなasciiコード列が入るとのこと。 ヘッダーについては、IDトークンでは現在のところRS256が使われているようです。
kidは署名に用いられた鍵を表しており、Googleが公開しているDiscoveryのjwks_uriから取得できる鍵リストの中から、一致するものをdecodeに用います。
この鍵リストは定期的に変わるようなので、cacheするとしても一定期間で取り直した方が良さそうです。IDトークンの検証手順
IDトークンの検証手順についても公式で以下の5stepで説明されています。
- Google発行の証明書が用いられているか検証する
- issクレームがgoogleのもの (
https://accounts.google.com
またはaccounts.google.com
) か検証する- audクレームが送信側のプロダクトごとに設定される項目と一致するか検証する
- expクレームが有効期限内か検証する
- hdパラメータを設定している場合、hdクレームが正しいか検証する
この他、サービスアカウントの場合はemailクレームも想定したものか検証した方が良さそうです。
実装
rubyでやる場合は
googleauth
gem(v0.13.0以降)を利用すると便利です。
自前でやる場合は証明書の管理なども面倒ですが、その辺も全部やってくれます。endpointを指定するということでサーバーが必要になるので、今回はRailsで書きました。
Railsで認証を行う場合はControllerにおいて、
ActionController::HttpAuthentication::Token::ControllerMethods
をincludeすると
authenticate_with_http_token
(自前で例外など処理する)authenticate_or_request_with_http_token
(失敗時の処理はお任せ)などで簡単にtokenが取得できるようになります。
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base include ActionController::HttpAuthentication::Token::ControllerMethods before_action :authenticate! private attr_reader :oidc_token_hash def authenticate! authenticate_or_request_with_http_token do |token, _options| @oidc_token_hash = Google::Auth::IDTokens.verify_oidc(token, aud: request.url) @oidc_token_hash['email'] == ENV.fetch('GCP_SERVICE_ACCOUNT_EMAIL') # 設定値の管理はENV以外でもOK rescue Google::Auth::IDTokens::VerificationError => _e false end endaud/iss/(azp)のクレームはgem側でやってくれるため、emailを独自にチェックするだけで済みました。
例外処理ですが、verify_oidcはトークンがおかしい場合などにVerficationError、公開鍵周りの問題でKeySourceErrorを発生させます。
前者は入力側の問題なので400系(ここではfalseを返すので401になる)として返しておき、後者は公開鍵取得に失敗した等クライアント側はどうしようもケースということで500系として検知できるようにしておきました。
この辺りの例外の取り扱いは提供するサービスのポリシーに合わせてください。今回は例だったのでhtmlを返す形を取っていますが、通常GCPからのリクエストを処理したい場合はapi的な処理が多いと思いますので、
ActionController::API
を継承しつつauthenticate_with_http_token
で自前で処理するのも良いと思います。3. 送信側のプロダクトにサービスアカウントを紐付ける
続いて作成したサービスアカウントを送信側プロダクトに紐付けます。
今回は例としてCloud Schedulerを取り上げます。Cloud Schedulerの場合はAuthヘッダーでOIDCトークンを選択するところが重要です。(公式ドキュメントはこちら)
ターゲットはHTTPを選択肢、URLには受信側のendpointを指定します。
またAuthヘッダーではOIDCトークンを選択し、サービスアカウントには最初に作ったアカウントを指定します。なお、ターゲットのURLが
*.googleapis.com
なGoogle APIのときは、AuthヘッダーにOAuthトークンを使用するようです。一番下にある対象の項目は後の
aud
クレームの値になります。
空欄の場合はターゲットのURLが入ります。後はcronでも手動でもいいので実行し、認証が成功するかを確認します。
Cloud Pub/SubやCloud TasksなどもHTTPターゲットの設定とサービスアカウントが設定できるので、同様の設定で大丈夫です。
おわりに
ということでGCPの非同期系プロダクトからのリクエストを認証する設定の流れについてでした。
受信側にもし認証機構をもつプロダクトを割り当てられる場合はそちらを選択した方が楽ではありますが、idトークンの認証自体もそれほど複雑ではないので、ちゃんと導入してセキュアな状態を保ちたいですね。
- 投稿日:2020-12-08T22:31:25+09:00
Ruby3.0で導入される型定義!TypeprofでBlockやProcを解析してみる
この記事はRuby 3.0 Advent Calendar 2020の14日目の記事です。Rubyに導入される型定義でProcがどのように書かれるのかについてまとめています。
はじめに
Ruby3.0ではコードに型定義を与えるRBSという仕組みが加わります。Rubyで動いているプロダクトの可読性や保守性を高めるためにこの型定義の導入を考えている方も多いのではないでしょうか。
RBSによってRubyのコードに型情報を与えるための手助けをしてくれるツールとして静的解釈に基づく型解析ツールであるTypeprofがあります。RBSやTypeprofの基本的な解説は色々な記事が出ているため、本記事ではドキュメントなどにあまり載っていないTypeprofを用いたBlockやProcの型推論に関してまとめました。使用した環境は以下の通りです
ruby 3.0.0preview1 (2020-09-25 master 0096d2b895) [x86_64-darwin18]
typeprof (0.9.0)
steep (0.37.0)
rbs (0.20.1)準備
Procを用いた簡単なコードを準備します。
# lib/app.rb class App def foo(n) n.to_s end def boo(fn) fn.call end def baz(fn) fn.call(5) end def bar(&fn) fn.call(0) end end proc1 = proc { "ブロック" } proc2 = Proc.new { |n| n.to_s } app = App.new p app.foo(5) p app.boo(proc1) p app.baz(proc2) p app.bar { |a| a.to_s }まず実行してみます。
$ ruby lib/app.rb "5" "ブロック" "5" "0"問題なく実行できました。それではtypeprofで型解析を行います。
$ typeprof lib/app.rb # Revealed types # lib/app.rb:24 #=> String # lib/app.rb:25 #=> String # lib/app.rb:26 #=> untyped # lib/app.rb:27 #=> String # Classes class App def foo: (Integer) -> String def boo: (^-> String) -> String def baz: (Proc) -> untyped def bar: { (Integer) -> String } -> String end型解析結果が出てきました。おおよその型をコードを実行することで判断してくれます、自分で一からRBSを書く手間が省けてありがたいですね。TypeprofのドキュメントによるとProcに関しては抽象化せず基本的に渡される引数と返される値を元に具体的な型を出力するようです。
Procオブジェクトは、ラムダ式(-> { ... })やブロック仮引数(&blk)で作られるクロージャです。 これらは抽象化されず、コード片と結びついた具体的な値として扱われます。 これらに渡された引数や返された値によってRBS出力されます。
Typeprocではコードを実行することで型情報の解析をするため、どのような分析結果が出るかはコードの使用例によって異なります。今回のコードではbooには引数なしでStringを返すprocを与えているため(^-> String) -> Stringと分析されていますが、例えば以下のようにこれをintを返すprocに変更すると、typeprofの分析結果は変わります。
proc1 = proc { 1 } p app.boo(proc1) #=> def boo: (^-> Integer) -> Integerまた、これら両方を与えると、出入力両方がユニオンタイプの型シグネチャになります。
proc1 = proc { "ブロック" } proc2 = proc { 1 } p app.boo(proc1) p app.boo(proc2) #=> def boo: (^-> (Integer | String)) -> (Integer | String)Procやblockの型シグネチャは以下のように行います。詳細はRBSのsyntaxドキュメントを参照してください。
# Proc ^(Integer) -> String ^(?String, size: Integer) -> bool #Block { (Integer) -> (Integer | String) }booに関してはうまく入出力の型の分析ができていますが、bazに関しては入力がProcであることしか分析できていませんね。ドキュメントを調べていくと以下のような文を見つけました。
Class.newは対応されません(untypedを返します)。
これが直接の原因かは検証できていませんが、上記のbazに与えるproc2もproc関数で書いてみます。
proc2 = proc { |n| n.to_s } p app.baz(proc2) # => def baz: (^(Integer) -> String) -> String今度はちゃんと型が分析されましたね。
それでは、この結果をそのままRBSファイルにしていきます。RBSファイルの拡張子は.rbsです。# sig/app.rbs class App def foo: (Integer) -> String def boo: (^-> String) -> String def baz: (Proc) -> untyped def bar: { (Integer) -> String } -> String endではこの型情報を元に実行ファイルの型情報のチェックを行っていきます。型シグネチャを用いた静的チェックにはsteepというgemを使います。以下のコマンドでsteepのための設定ファイルを作成します。
$ steep init生成されたSteepfileを以下のように変更します。signatureでRBSの型シグネチャファイルのあるディレクトリを、checkでチェックしたいrbファイルのディレクトリを指定します。
target :lib do signature "sig" check "lib" end型情報の静的チェックを行います。
$ steep check lib/app.rb:25:10: ArgumentTypeMismatch: receiver=::App, expected=^() -> ::String, actual=::Proc (proc1) lib/app.rb:26:10: ArgumentTypeMismatch: receiver=::App, expected=^(::Integer) -> ::String, actual=::Proc (proc2)型チェックのエラーが出ましたね。どうやらsteepではまだProcの引数のチェックまではできず、Procとプロック型シグネチャを不一致と見なしてエラーを吐いてしまうようなので、以下のように書き換えます。
# sig/app.rbs class App def foo: (Integer) -> String def boo: (Proc) -> String #<=ここを編集 def baz: (Proc) -> String #<=ここを編集 def bar: { (Integer) -> String } -> String endもう一度型チェックを行います。
$ steep check今度はエラーが出ませんでした。steepがprocの引数の型までチェックしてくれるようになるといいですね。
最終的なフォルダ構成は以下のようになっています。また、使用したコードはhttps://github.com/TomeHirata/typeprof_testに上がっています。
まとめ
Ruby3.0で標準となるRBSやTypeprofを用いてrubyコードの型プロファイリングを行なってみました。自動で既存のRubyコードからRBSの型定義を分析してくれるTypeprofとても便利そうですね!Typeprofでおおよその型定義を自動生成して一部手直しすることで簡単に型定義が用意できるようになりそうです。
RBSではProcとBlockはそれぞれ以下のように書きますが、本記事執筆時点ではTypeprofでClass.newをおってくれなかったり、steepでprocの型不一致エラーが起きたりしました。# Proc ^(Integer) -> String #Block { (Integer) -> (Integer | String) }まだ、型シグネチャの書き方や解析ツールの推論部分などにわかりにくい部分はありますが、今後改善されていくと思います。皆さんもぜひこの機会に型を持ったRubyライフを始めてみてください。また、このように書くとsteepやTypeprofでProcの定義がうまくいくよと言ったTipsがあればぜひ教えてください!!
最後に
現在 estie では, JavaScript や Ruby に強いエンジニアを積極採用中です!!
不動産のデータを使ってデータプラットフォームを構築したい、分析したい、プロダクトを作ってみたいという方はぜひ!
- 投稿日:2020-12-08T22:06:05+09:00
【Rails】ymlファイルの中でerbを使い、動的に値を取得してrakeタスクの引数にする
- 投稿日:2020-12-08T21:27:24+09:00
deviseを利用した新規登録時のユーザー情報の保存とフラッシュメッセージについて
はじめに
deviseを利用したuser周りの設定の中で、私が困ったことについて解決方法を記述しようと思います。
今回なかなか思った通りにできずに困ったことはユーザーの新規登録です。新規登録ページの作成はできましたが、そこから先で躓いてしまいました。
- 登録したい新規ユーザーの情報が登録されない
- 新規登録ページで情報入力後、ボタンを押してもページが変わらない
- 新規登録ができた場合とできなかった場合の動作を変えたい
- 新規登録時のフラッシュメッセージの表示がされない
以上を解決した方法について備忘録として記事を書きました。
Rails 5.2.4.4
Ruby 2.5.1
を使用しています。ルーティングの設定
最初に躓いたのはユーザー情報が保存されないことです。sendボタンを押すとそのまま動かなくなり、Sequel Proを確認しても情報の保存がされていませんでした。
ルーティングとコントローラーに問題があると考えたので、まずはroutes.rbを確認します。
現状ではおそらく既にご自身で設定したトップページなどへのルーティングと、devise導入時に自動で記述されたコードで以下のようになっていると思います。
config/routes.rbdevise_for :users root "トップページ" resources :その他のページここに追記をしていきます。
新規登録はdeviseで自動生成されたregistrationsにあたるので以下のように記述をします。新規登録には関係ありませんが、ついでにログインに必要なsessionsについても記述しておきましょう。また、resources :usersという記述も追加します。config/routes.rbdevise_for :users, controllers: { registrations: 'users/registrations', sessions: 'users/sessions' } root "トップページ" resources :その他のページ resources :usersコントローラーの設定
次にコントローラーを確認します。
registrations_controller.rbを開きます、こちらはdeviseで自動生成されたファイルです。
中を見ると class ~ end の中身が全てコメントアウトされているのが確認できます。こちらにコードを記述をしていきます。今回やりたいことはユーザー新規登録なので、newとcreateを記述します。
app/controllers/uses/registrations_controller.rbdef new @user = User.new end def create @user = User.new(user_params) if @user.save redirect_to root_path else render :new end end private def user_params params.require(:user).permit(:email, :password, :password_confirmation) endcreateの中では複数のことをしているので注意点の記述や解説をします。
まず
app/controllers/uses/registrations_controller.rb@user = User.new(user_params)この(user_params)についてははprivate以下で記述しています。
app/controllers/uses/registrations_controller.rbdef user_params params.require(:user).permit(:email, :password, :password_confirmation) endこのpermitの後の部分では新規登録時に必要なカラムを記述します。例えば私の場合はニックネームも登録できるようにしたかったので、以下のように記述をしました。
app/controllers/uses/registrations_controller.rbdef user_params params.require(:user).permit(:nickname, :email, :password, :password_confirmation) endご自身の登録したい情報によって記述を変更してください。
次の記述です。
app/controllers/uses/registrations_controller.rbif @user.save redirect_to root_path else render :new endこの記述は新規登録ができた場合とできなかった場合の条件分岐をしています。
こちらでは登録ができた場合はトップページへ飛び、できなかった場合にはまたユーザー新規登録ページへと戻ってくるように記述しています。redirect_to と render は似たような動きをしますが使い分けが必要です。こちらの記事がとてもわかりやすく解説されているので、気になる方は目を通してみると勉強になると思います。
https://qiita.com/morikuma709/items/e9146465df2d8a094d78ここまでで以下の問題が解決しました。
- 登録したい新規ユーザーの情報が登録されない
- 新規登録ページで情報入力後、ボタンを押してもページが変わらない
- 新規登録ができた場合とできなかった場合の動作を変えたい
新規登録ページから情報を入力して登録ができること、登録が失敗した場合にはまた新規登録ページに戻ってくることを確認してください。問題なく行えていれば成功です。
しかし今のままでは新規登録が成功してもトップページに飛ぶだけなので、ちゃんと登録ができたのかユーザーにはとてもわかりにくい状態です。
最後にフラッシュメッセージを表示できるようにします。フラッシュメッセージを表示する
deviseでは設定をすれば簡単にフラッシュメッセージを表示できます。
こちらの記事がフラッシュメッセージの導入方法についてわかりやすく解説してあります。
https://qiita.com/hari00702/items/4e100b9dc78d19e8e316しかし、ユーザー新規登録時の登録完了のメッセージはその中には入っていません。
そこでregistrations_controller.rbのcreateに以下のように追記します。app/controllers/uses/registrations_controller.rbdef create @user = User.new(user_params) if @user.save redirect_to root_path, notice: 'ユーザー新規登録を完了しました' #追記 else render :new end endnotice: の後ろの部分の記述がそのままフラッシュメッセージになるので好みのメッセージを入れてください。
新規登録ページから実際に登録を行ってみて、ページ上部にメッセージが表示されていれば成功です。これで残りの
- 新規登録時のフラッシュメッセージの表示がされない
についても解決できました。
参考
- 投稿日:2020-12-08T21:00:54+09:00
【Rails】既存のRails6プロジェクトにDockerを導入してみる
はじめに
既存のRailsプロジェクトをDockerに乗せていきます。
初学者による記事なので間違い等ございましたらご指摘いただけると幸いです。
※本記事は開発環境のみの導入を対象としています。参考
Dockerが何なのか全く分からない!という方はまずは下記の記事を参考にDockerを触ってみてください。
DockerをMacにインストールする
こちらの記事も手っ取り早くDockerをふんわり理解するのにオススメです。
Dockerについてなるべくわかりやすく説明する環境
- macOS Catalina 10.15.7
- Ruby 2.6.5
- Rails 6.0.3.3
- Docker 19.03.13
- docker-compose 1.27.4
- MySQL 5.6.47
目次
- 必要ファイルの作成
- ファイル構成
- 作成したファイルの編集
- コンテナの起動
必要ファイルの作成
必要ファイルは以下の5つです。rails newをした際に自動生成されるファイルもあるので必要なもののみ作成していきましょう。
- Dockerfile
- docker-compose.yml
- Gemfile
- Gemfile.lock
- database.yml
ファイル構成
既存のアプリケーション名 └── app ├── Dockerfile # 作成 ├── docker-compose.yml # 作成 ├── Gemfile ├── Gemfile.lock ├── config └──database.yml作成したファイルの編集
Dockerfile
DockerfileFROM ruby:2.6.5 #自身のrubyバージョンを指定 (ruby -v) RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update && apt-get install -y --no-install-recommends\ nodejs \ yarn \ mariadb-client \ build-essential \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* WORKDIR /myproject COPY Gemfile /myproject/Gemfile COPY Gemfile.lock /myproject/Gemfile.lock RUN gem install bundler RUN bundle install RUN yarn install --check-files COPY . /myprojectRails6でwebpackerが標準になったことでyarnのインストールが必要になってきます。
Docker Hubで確認するとrubyのイメージはDebian系となっているので、yarnの公式ドキュメントに記載されているOS毎のインストール方法を参考に記述していきます。
ちなみに、yarnの公式ドキュメントは日本語表示のままだとなぜかOSの選択肢の中にMacとWindowsしか表示してくれないので英語表示に切り替えて確認しましょう。docker-compose.yml
docker-compose.ymlversion: '3' services: db: #データベースのコンテナ作成 image: mysql:5.7 #自身のmysqlバージョンを指定 (mysql --version) command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci ports: - '3306:3306' volumes: - mysql-data:/var/lib/mysql environment: MYSQL_DATABASE: myapp_development #プロジェクト名_development MYSQL_ROOT_PASSWORD: password MYSQL_USER: root MYSQL_PASSWORD: password web: #アプリケーションのコンテナ作成 build: context: . dockerfile: Dockerfile command: bundle exec rails s -p 3000 -b '0.0.0.0' tty: true stdin_open: true depends_on: - db ports: - "3000:3000" volumes: - .:/myproject volumes: #dbを永続化するための記述 mysql-data:Gemfile
Gemfilesource 'https://rubygems.org' gem 'rails', '~>6.0.0'既存アプリケーションの場合この記述はすでにあるはずですが念のため確認しておきましょう。
database.yml
database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root #docker-compose.ymlで指定したユーザー名 password: password #docker-compose.ymlで指定したパスワード development: <<: *default database: myapp_development #プロジェクト名_development host: db #docker-compose.ymlでデータベースのコンテナ作成部分で指定したservice名 test: <<: *default database: myapp_test #プロジェクト名_test host: db #docker-compose.ymlでデータベースのコンテナ作成部分で指定したservice名アプリケーションのコンテナからデータベースのコンテナに接続するための設定を記述していきます。
セキュリティ面を気にする場合はusernameとpasswordに環境変数を設定しましょう。今回は開発環境のみということもあり設定していません。コンテナの起動
ターミナルcd myapp docker-compose build #コンテナを建てる docker-compose up -d #コンテナ起動(-dオプションをつけることでバックグラウンドで実行)これで localhost:3000 にアクセスするとページが表示されます◎
- エラーが発生した場合は
ターミナルdocker-compose logs -f
でリアルタイムでログの確認ができるのでエラー原因を探して解決しましょう。
- サーバーを停止させたいときは以下のコマンドを実行してください。
ターミナルdocker-compose down
最後に
実装に思いの外手間取ったので備忘録的に記事にしてみました。
どなたかの参考になれば幸いです。
- 投稿日:2020-12-08T20:58:10+09:00
formオブジェクトで複数テーブルへ値の保存
formオブジェクトを使った複数テーブルへの値の保存を1週間位試行錯誤してやっと実装できたので、記事に残しておこうと思います。
色々な方の実装方法を真似て作ったので解釈が間違っているところや、理解が不十分なところもありますが、自分はこう解釈してこの記述をしているということで書いていきます。前提 プログラミング初学者の備忘録的な感じで書いており、間違いがある場合がありますので、ご注意ください。 解釈やそもそもの定義など間違っている部分等ありましたらご指摘していただけると幸いです。 ターミナルでのファイル生成の記述については全て省略しています。動作環境
macOS Catalina 10.15.7
Rails 6.0.3.4
Ruby 2.6.5p114formオブジェクトって何?
デザインパターンの1つで、1つの投稿フォームから複数のモデルに関連するデータを更新できるものです。
簡単に言うと、1つの投稿フォームから複数のテーブルへの保存の処理をするまとめ役みたいな感じです。
そのため、formオブジェクトにはform_withメソッドに対応する機能と、バリデーションを行う機能をもたせることが必要になります。
form_withで複数のテーブルに保存するデータをformオブジェクトに送り、
届いたデータに対して、各モデルのバリデーションをformオブジェクトで行った後に複数のテーブルへデータを保存すると言った流れです。formオブジェクトを使ったフォームの実装
今回は以下2つをポイントに実装を行いました。
formオブジェクトで記事の新規投稿、更新処理ができる。
1つの入力フォームから複数のタグの新規登録、更新処理ができる。
記事の新規投稿のみに比べ、更新処理も実装するとかなり手間がかかりました。ER図と実際の投稿フォーム
ER図はこのような感じで、赤枠の部分をformオブジェクトで実装しました。
formオブジェクトを使い、1つの投稿フォームでarticleとtagを保存するような設計です。
具体的な動きとしては、下図のようなフォームで記事にタグを付けて投稿し、記事とタグをそれぞれのテーブルに保存します。
ArticleとTagを紐付けるために中間テーブルとして、article_tag_relationsテーブルを作っています。モデルについて
各モデルは以下のように記述しました。
マイグレーションファイルもカラムの部分のみをモデルの下に書いています。Articleモデル(app/models/article.rb)
class Article < ApplicationRecord has_many :article_tag_relations, dependent: :destroy has_many :tags, through: :article_tag_relations end マイグレーションファイル t.string :title, null: false t.text :output, null: false t.string :action t.integer :user_idTagモデル(app/models/tag.rb)
class Tag < ApplicationRecord has_many :article_tag_relations, dependent: :destroy has_many :articles, through: :article_tag_relations validates :tag_name, uniqueness: true end マイグレーションファイル t.string :tag_name, uniquness: trueArticleTagRelationモデル(app/models/article_tag_relation.rb)
class ArticleTagRelation < ApplicationRecord belongs_to :article belongs_to :tag end マイグレーションファイル t.references :article, foreign_key: true t.references :tag, foreign_key: trueモデルについては2つのポイントがあり
dependent: :destroy
1つは、articleが削除されたとき、tagが削除されたときには中間テーブルのarticle_tag_relationから削除された値が含まれるレコードも削除されるようにしました。
validates :tag_name, uniquness: trueもう1つは、同じ名前のtagが複数保存されないようにしています。
コントローラーについて
コントローラーは以下のように記述しました。
class ArticlesController < ApplicationController before_action :authenticate_user!, only: [:new, :edit, :update, :destroy] before_action :set_article, only: [:show, :edit] def index @articles = Article.all.order('created_at DESC') end def new @article_tag = ArticleTag.new end def create @article_tag = ArticleTag.new(article_params) tag_list = params[:article][:tag_name].split(',') if @article_tag.valid? @article_tag.save(tag_list) redirect_to articles_path else render :new end end def show end def edit @article = Article.find(params[:id]) @article_tag = ArticleTag.new(article: @article) end def update @article = Article.find(params[:id]) @article_tag = ArticleTag.new(article_params, article: @article) tag_list = params[:article][:tag_name].split(',') if @article_tag.valid? @article_tag.save(tag_list) redirect_to article_path(@article) else render :edit end end def destroy @article = Article.find(params[:id]) redirect_to root_path if @article.destroy end private def article_params params.require(:article).permit(:title, :output, :action, :user_id, :article_id, :tag_name, :tag_id).merge(user_id: current_user.id) end def set_article @article = Article.find(params[:id]) end endコントローラーについては特に変わったところはなく、
tag_listがformオブジェクトで値を保存するために定義したsaveメソッドに使われるくらいです。formオブジェクトの作成
formオブジェクトはapp/formsディレクトリを作成し、その直下にarticle_tag.rbというファイル名で作成しました。
class ArticleTag include ActiveModel::Model attr_accessor :title, :output, :action, :tag_name, :user_id, :tag_id, :article_id with_options presence: true do validates :title, length: { maximum: 40 } validates :output, length: { maximum: 400 } end # レコードに値があるかないかでcreateかupdateかに分岐させる delegate :persisted?, to: :article def initialize(attributes = nil, article: Article.new) @article = article attributes ||= default_attributes super(attributes) end def save(tag_list) ActiveRecord::Base.transaction do @article.update(title: title, output: output, action: action, user_id: user_id) current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil? old_tags = current_tags - tag_list new_tags = tag_list - current_tags old_tags.each do |old_name| @article.tags.delete Tag.find_by(tag_name: old_name) end new_tags.each do |new_name| article_tag = Tag.find_or_create_by(tag_name: new_name) @article.tags << article_tag article_tag_relation = ArticleTagRelation.where(article_id: @article.id, tag_id: article_tag.id).first_or_initialize article_tag_relation.update(article_id: @article.id, tag_id: article_tag.id) end end end def to_model article end private attr_reader :article, :tag def default_attributes { title: article.title, output: article.output, action: article.action, tag_name: article.tags.pluck(:tag_name).join(',') } end endinclude ActiveModel::Model
ActiveModel::Modelというモジュールをincludeメソッドで与えます。
この記述によって ArticleTagクラスがモデルとしての機能を行えるようになります。
form_withへの対応やバリデーションを行うために必要な記述です。delegateメソッド
指定したオブジェクトにメソッドの実行を委譲させるものです。
委譲:あるオブジェクトの操作を一部他のオブジェクトに代替させる手法
言葉が難しいです。。。delegate :メソッド名, to: :委譲先のオブジェクトここではpersisted?メソッドをarticleというオブジェクトに委譲しています。
to_modelメソッドと合わせて、
レコードに値が存在しないときにcreateアクション
レコードに値が存在するときはupdateアクションを動かすために必要な記述になります。to_model
モデルであるためには、to_modelを定義する必要があります。
コントローラーやview helperにモデルが渡ったときにto_modelを呼んでモデルを操作するためです。理解が浅いためうまく説明できないのですが、delegateメソッドで値があるときと無いときに応じてPOSTやPATCHの処理を切り替え、
to_modelメソッドはアクションのURLを適切な場所に切り替えているということらしいです。initializeメソッド
initializeメソッドはnewメソッドでインスタンスを生成する時に初期値で設定する値などを定義するメソッドです。
attributesは属性値の意味で、attributes ||= default_attributes super(attributes)arrtibutesが存在すればその値を、nilであれば、default_attributesをattributesに代入するといった記述です。
superは、スーパークラスを呼び出す記述です。
initializeをここでは再定義(オーバーライド)していますが、オーバーライドする前のinitializeメソッドを引数をattributesとして呼び出しています。
単にnewメソッドでインスタンスを生成するわけではなく、
attributesが存在すればその値を使ってインスタンスを生成
存在しなければ、default_attributesを使ってインスタンスを生成するといった記述です。
createアクションとupdateアクションを値のあるなしで使い分けるための記述を1つにまとめるために、initializeの再定義を行っていると思います。
投稿のみであれば、ここの記述は必要ありません。
更新にも対応するためにレコードに保存された値を取得するために定義しています。default_attributes
privateメソッド以下にあるdefault_attributesは投稿フォームに入力する値のdefault値を定義しました。
articleとtagを使ってdefalut値を設定するためにattr_readerでarticleとtagを読み込んでいます。
ここでは書き込む必要はなく、値として読み込むだけの処理のためattr_readerで十分になります。saveメソッド
saveメソッドは新規投稿と更新の両方をこのメソッド1つで行うことができます。
initializeを再定義したので、newアクションでdefault_attributesの値が入ったレコードが生成されます。
フォームの入力値をarticle_paramsとして取り出して、以下のコードで入力した値に更新するという処理にして新規登録します。@article.update(title: title, output: output, action: action, user_id: user_id)新規登録の場合は、defalut_attributesとして全ての値がnilのレコードが生成され、フォームの入力値(article_params)で更新するといった流れです。
更新処理の場合は、保存されたレコードの値がdefault_attributesとしてあり、フォームの入力値(article_params)で更新すると言った流れです。
タグの部分の記述については後述します。ActiveRecord::Base.transaction
トランザクションの処理を記述する時に使うものです。
トランザクションとは、分割できないワンセットの処理単位のことです。
この中に書かれた処理で途中で例外処理(エラー)があったときには途中までの処理や結果はやらなかったことにするというものです。
ここではActiveRecord::Base.transaction doからendまでの処理
つまり、articleとtagの新規登録、更新処理がトランザクションになっています。
articleとtagの新規登録や更新する時にどこかで処理が失敗した場合、途中までやっていた処理はすべてなかったことにするということです。タグの扱いについて
タグについては複数のタグを登録、編集できる機能にしました。
tag_list(フォームに入力したタグ)
current_tags(現在保存されているタグ、更新の場合のみ使われる変数)
old_tags(現在保存されていて、そのまま残すタグ)
new_tags(新しく保存されるタグ)
の4つを配列で定義し、配列内の各要素を保存するという流れです。tag_list = params[:article][:tag_name].split(',')上記の記述では、フォームで送信されたparamsからタグの値を取り出します。
このときsplit(',')では入力した値を,で区切って要素に分解して配列にするといった処理が行われます。
例えばタグを入力するフォームに朝,昼,夜と入力した場合、
tag_list = ["朝","昼","夜"]と入ることになります。
新規登録の場合
current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?unless @article.tag.nil?
となっており、タグが空でない場合にcurrent_tagsが定義されるため新規登録の場合は定義されません。
new_tags = tag_listとなり以下の処理に移ります。new_tags.each do |new_name| article_tag = Tag.find_or_create_by(tag_name: new_name) @article.tags << article_tag article_tag_relation = ArticleTagRelation.where(article_id: @article.id, tag_id: article_tag.id).first_or_initialize article_tag_relation.update(article_id: @article.id, tag_id: article_tag.id) end2行目:find_or_create_byメソッドではTagモデルを通じてTagsテーブルから、tag_nameがnew_nameのものを探し、なければその値を保存します。
3行目:次の行で保存されたarticle_tagを@article.tags、つまり投稿した記事のタグの配列に格納します。
4行目:中間テーブルに値を保存する処理です
first_or_initializeメソッドは新規登録の場合はinitializeつまり新しくレコードが生成され、更新の場合はレコードは生成されません。
5行目:生成したレコード、元々あったレコードをupdateメソッドで更新する
と言った流れで新規登録されます。更新の場合
更新の場合はtag_listとcurrent_tagsを使って、
編集された時に削除されたタグを中間テーブルから削除し、新しく追加されたタグをTagsテーブルと中間テーブルに保存する必要があります。current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil? old_tags = current_tags - tag_list new_tags = tag_list - current_tags例として、元々登録していたタグをtag1,tag2とします。
1行目:投稿した@articleに紐づくtag達を配列形式で取得します。
tagsテーブルからpluckメソッドでtag_nameというカラムを指定し、投稿した記事につけたタグのtag_nameの値を配列に格納します。
2行目:元々登録していたけれど、編集によって削除されたタグをold_tagと定義しています。
例えば、元々tag1,tag2があって編集画面でタグの欄をtag1だけにした場合はtag_listはタグのフォームに入力された値であるため、
old_tags = ["tag1", "tag2"] - ["tag1"] =["tag2"]となります。
3行目:元々登録されていなかった新規のタグをnew_tagsと定義しています。
例えば、元々tag1,tag2があって編集画面でtag1,tag2,tag3とした場合、
new_tags = ["tag1", "tag2", "tag3"] - ["tag1", "tag2"] = ["tag3"]となります。old_tagsについての処理
old_tagsはフォームから削除され、投稿につけなくなったタグです。
投稿に紐づくタグとして以下の記述で削除する必要があります。old_tags.each do |old_name| @article.tags.delete Tag.find_by(tag_name: old_name) endnew_tagsについての処理
new_tagsは新たに追加したタグなので、新規のタグの場合はTagsテーブルに保存する必要があります。
また、投稿に紐づくタグとして新たに中間テーブルに保存する必要があります。
処理の内容については新規登録の場合の説明と全く一緒です。まとめと感想
簡単なまとめ
formオブジェクトは1つのフォームから複数のテーブルに値を保存するために使われるデザインパターンの1つ
複数のタグを保存するにはpluckメソッドやsplitメソッドをうまく使って配列に格納し、eachメソッドを使ってそれぞれのタグに保存処理を行う感想
delegateとto_modelメソッドについてなんとなく意味は分かった気がするが、formオブジェクトに記述して細かい部分でどう動いているのか完全には理解できていないので、もう少し理解を深める。
to_modelメソッドはActiveModel::Conversationに含まれるメソッドということで、他にもよく使っているメソッドがあるため今後勉強していく。実装内容をすべて書いたのでものすごく長くなりました。
解釈間違い等ありましたらコメントしていただけると幸いです。
記事にわかりやすくまとめる技術も学んでいかなければ。。。参考記事
formオブジェクトについて
https://product-development.io/posts/rails-design-pattern-form-objects
https://tomo-bb-aki0117115.hatenablog.com/entry/2020/10/29/232822タグ付け機能
https://qiita.com/E6YOteYPzmFGfOD/items/bfffe8c3b31555acd51dトランザクションについて
https://wa3.i-3-i.info/word142.html
- 投稿日:2020-12-08T20:57:18+09:00
Ruby で解く AtCoder Educational DP Contest D ナップサック問題
はじめに
AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。今回のお題
AtCoder Educational DP Contest - D - Knapsack 1
Difficulty: ---今回のテーマ、動的計画法
以前の投稿 Ruby の Hash における keys.each と each_key の違い では、ハッシュや配列にマーク代わりとして
1
を代入していましたが、ナップサック問題は重さ(weight)と価値(value)の二次元になりますので、価値を代入していきます。Array コピー
ruby.rbn, mw = gets.split.map(&:to_i) p = n.times.map { gets.split.map(&:to_i) } dp = Array.new(mw + 1, 0) p.each do |w, v| dp_tmp = dp.dup w.upto(mw) do |i| dp_tmp[i] = dp[i - w] + v if dp[i] < dp[i - w] + v end dp = dp_tmp end puts dp[mw]insert.rbdp_tmp[i] = dp[i - w] + v if dp[i] < dp[i - w] + v
1
の代わりに、価値を代入しています。Array 後ろから
ruby.rbn, mw = gets.split.map(&:to_i) p = n.times.map { gets.split.map(&:to_i) } dp = Array.new(mw + 1, 0) p.each do |w, v| mw.downto(w) do |i| dp[i] = dp[i - w] + v if dp[i] < dp[i - w] + v end end puts dp[mw]詳しい内容は、けんちょんさんの ナップサック DP を in-place 化 ここら辺を参照願います。
Array コピー Array 後ろから コード長 (Byte) 257 221 実行時間 (ms) 852 864 メモリ (KB) 75024 15044 Hash版もトライしたのですが、うまくいかなかったです。
まとめ
- D - Knapsack 1 を解いた
- Ruby に詳しくなった
- 投稿日:2020-12-08T20:44:27+09:00
Ruby インスタンス変数を外部から操作する パート2
昨日の記事Ruby インスタンス変数を外部から操作するの続きです
attr_accessorメソッド
単純にインスタンス変数の内容を外部から読み書きする場合は、
arrt_accessorメソッドを使うclass User #@nameを読み書きするメソッドが自動的に定義される attr_accessor :name def initialize(name) @name = name end #nameメソッドや、name=メソッドを明示的に定義する必要がない end user = User.new('太郎') #@nameを変更する user.name = 次郎 user.name #=> "次郎"複数指定することも可能
class User #@nameを読み書きするメソッドが自動的に定義される attr_accessor :name, :age def initialize(name) @name = name @age = age end #nameメソッドや、name=メソッドを明示的に定義する必要がない end user = User.new('太郎', 20) user.name #=> '太郎' user.age #=> 20attr_renderメソッド
インスタンス変数の内容を読み取り専用にしたい場合は、
arrt_renderメソッドを使うclass User #読み取り用のメソッドだけを自動的に定義する attr_render :name def initialize(name) @name = name end end user = User.new('太郎') #@nameを参照する user.name #=> "太郎" user.name = '次郎' #=> NoMethodErrorattr_writerメソッド
インスタンス変数の内容を書き込み専用にしたい場合は、
arrt_writerメソッドを使うclass User #書き込み用のメソッドだけを自動的に定義する attr_writer :name def initialize(name) @name = name end end user = User.new('太郎') #@nameは変更できる user.name = "次郎" #@nameの参照はできない user.name #=> NoMethodError参考記事、書籍
チェリー本
- 投稿日:2020-12-08T20:18:22+09:00
CentOS 8にRuby 2.7をインストール(AppStream)
はじめに
Application Stream(AppStream)を利用してCentOS8にRuby 2.7をインストール
参考:RHEL8のパッケージ構成 - BaseOSとApplication Stream - 赤帽エンジニアブログ
第4章 新機能 Red Hat Enterprise Linux 8 | Red Hat Customer Portalサポート
本手法で導入した場合、Red Hat Enterprise Linux 8 Application Streams Life Cycle - Red Hat Customer Portalより、2023-05がEOLだと思われる。
それ以降に報告された脆弱性や不具合への対応は実施されない可能性がある。LOG
インストール
# cat /etc/redhat-release CentOS Linux release 8.3.2011 # yum module install ruby:2.7 ... 略各種確認
# which ruby /usr/bin/ruby # ruby -v ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
- 投稿日:2020-12-08T19:39:43+09:00
RailsでHTMLファイルを出力する
やりたいこと
テンプレートに沿ったHTMLファイルを出力したい!
前提条件
以下のコマンドでBooksControllerを作成し、その中にHTMLファイルを出力するアクションを作成する。
rails g controller booksテンプレートファイルを作成する
今回は以下のディレクトリを作成し、その中にテンプレートファイルを保存する。
app/views/books/templatehtml_template.html.erb<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>本の詳細</title> </head> <body> <table> <tr> <th>タイトル</th> <td><%= @title %></td> </tr> </table> </body> </html>アクションを作成する
以下のようなアクションを作成する。
books_controller.rbdef htmlfile_download @title = "本の題名" # 指定したファイルの中身を文字列で返す # layoutオプションの値をfalseにしておくと、レイアウトが適用されていない状態で取得できる template = render_to_string('books/template/html_template', layout: false) # HTMLファイルを生成 send_data(template, filename: "ファイル名.html") end以上です。
おまけ
render_to_stringで取ってきた値は以下のようになっています。
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>本の詳細</title> </head> <body> <table> <tr> <th>タイトル</th> <td>本の題名</td> </tr> </table> </html>
- 投稿日:2020-12-08T19:19:59+09:00
Active Storageで動画をアップする!!
Active Storageで動画をアップする!!
Active Storageを使用。動画をアップロードし、validatesで形式を指定する!!
お師匠方初めまして!
Active Storageはかなり便利で画像の際かなり使い勝手が良いですよね。
今回は動画にチャレンジでバリデーションまで設定しました。
他にも皆さんのオススメなどあったら聞きたいです^^https://gyazo.com/ab51ad2729703e1b77f831205fef7550
該当するソースコード
#app/models/post.rb class Post < ApplicationRecord belongs_to :user has_many :comments, dependent: :destroy has_one_attached :video with_options presence: true do validates :title validates :price, format: { with: /\A[-]?[0-9]+(\.[0-9]+)?\z/} validates_inclusion_of :price, in: 500..5000 validates :video end validate :video_type private def video_type if !video.blob.content_type.in?(%('video/quicktime video/quicktime')) errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください') end end end#app/views/posts/index.html.erb class PostsController < ApplicationController #省略 <video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>
- 投稿日:2020-12-08T19:19:59+09:00
Active Storageで動画をアップ
★Active Storageは画像、動画が投稿できる
❶バリデーションに形式を記述
#app/models/post.rb class Post < ApplicationRecord #省略 validate :video_type private def video_type if !video.blob.content_type.in?(%('video/quicktime video/quicktime')) errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください') end end end❷ビューファイルにvideoタグを記述
#app/views/posts/index.html.erb class PostsController < ApplicationController #省略 <video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>
- 投稿日:2020-12-08T19:19:59+09:00
Active Storageで動画投稿
★Active Storageは画像、動画が投稿できる
❶バリデーションに形式を記述
#app/models/post.rb class Post < ApplicationRecord #省略 validate :video_type private def video_type if !video.blob.content_type.in?(%('video/quicktime video/quicktime')) errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください') end end end❷ビューファイルにvideoタグを記述
#app/views/posts/index.html.erb class PostsController < ApplicationController #省略 <video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>★Active Storageは便利…
★ 教科書
- 投稿日:2020-12-08T18:06:33+09:00
rails コマンドへ独自コマンドを組み込む方法
Rake タスクって何だか変ですよね。テストも書きづらいし、できることなら書きたくないですよね。
Ruby には Thor というイケてる Gem があり、これを利用するとイケてるコマンドライン・ユーティリティを書くことができます。Thor はイケてるので、サブコマンドも書くことができます。Thor を利用する場合、コマンドライン・ユーティリティは、Thor クラスを継承したクラスとして作成するので、テストも簡単で、特別な知識は不要です。
Rake タスクではなくて、Thor を利用できたら素敵ですよね。
実は rails コマンドは Thor をすでに利用しているんです。そして、Rails エンジンの場合、Thor を使って独自コマンドを提供する標準的な方法が用意されているようですが、Rails アプリケーションの場合、標準的な方法はありません。自分でなんとかするしかありません。
以降では自分でなんとかする方法を説明します。generator のアシストは受けられないので、全て手作業でファイルを修正したり、ファイルを作成したりする必要があります。
前提
本記事で作成したサンプルは https://github.com/sunny4381/rails_command_extension に置いておきます。
このサンプルは Rails チュートリアルの第14章のソースコードを元にしています。Rails チュートリアルではUser
とMicropost
の二つのモデルが登場しますので、本記事もこの2つのモデルを操作してみたいと思います。Rails::Command::Base
早速、独自コマンドを作成していきます。独自コマンドは、直接 Thor を継承せずに、Rails が Thor をラップしたクラス Rails::Command::Base を提供しているので、このクラスを継承するようにします。
早速、独自コマンドを実装しましょう。以下のような
main_command.rb
ファイルを作成し、このファイルに独自コマンドを実装していきます。lib/commands/main/main_command.rbrequire "rails/command" module SampleApp module Command class MainCommand < Rails::Command::Base namespace "sample" @command_name = "sample" def hello say "hello" end end end end
namespace
と@command_name
を指定して、コマンド名を明示的に指定しています。この 2 つの指定がなければsample_app:main
なんていう冗長なコマンド名になってしまいます。そして、
hello
というコマンドを実装しており、bin/rails sample_app:hello
と実行することを意図しています。コマンドの実装方法の詳細については、Thor の Wiki を参照ください。
bin/rails の変更
次に独自コマンドを rails コマンドに認識させる必要があります。このため
bin/rails
を修正して、独自コマンドを組み込みます。次のように修正します。bin/rails#!/usr/bin/env ruby APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' #### ↓↓↓↓↓↓↓↓追加 # 独自コマンドの組み込み require_relative '../lib/commands/main/main_command' #### ↑↑↑↑↑↑↑↑追加 # run rails command require 'rails/commands'独自コマンドを実装したファイル
lib/commands/main/main_command.rb
をrequire_relative
で読み込んでいます。独自コマンドを rails コマンドのコマンド一覧へ登録する処理がRails::Command::Base
にありますので、読み込むだけで rails コマンドに登録されます。試しに
bin/rails
を実行してみます。$ bin/rails ... routes runner sample_app:hello secret secrets:edit ...
多数のコマンドが出力されるので少しわかりづらいですが、rails の標準コマンドに混じって
sample_app:hello
と独自コマンドが表示されています。試しに独自コマンドを実行してみます。
$ bin/rails sample_app:hello hello
モデルの操作とサブコマンド
コンソールに文字列を表示するような単純な処理ならこのままでも問題ありませんが、Rails アプリケーションが初期化されていないので、モデルを検索したり、作成したり、削除したりすることはできません。
Rails アプリケーションの初期化方法と合わせて、モデルを作成する独自コマンドを
sample_app
のサブコマンドとして追加する方法をみていきます。まず、
main_command.rb
を修正してサブコマンドを追加します。lib/commands/main/main_command.rbrequire "rails/command" require_relative '../user/user_command' require_relative '../micropost/micropost_command' module SampleApp module Command class MainCommand < Rails::Command::Base namespace "sample_app" @command_name = "sample_app" subcommand "user", SampleApp::Command::UserCommand subcommand "micropost", SampleApp::Command::MicropostCommand end end endRails アプリケーションが初期化されていないので、クラスのオートロードは効きません。
require_relative
を用いて明示的にuser_command.rb
とmicropost_command.rb
を読み込む必要があります。クラスを読み込んだ後、Thor のsubcommand
命令でuser
とmicropost
という2つのサブコマンドを追加しています。
user
サブコマンドの実体user_command.rb
は次のように実装します。lib/commands/user/user_command.rbmodule SampleApp module Command class UserCommand < Rails::Command::Base desc "list", "list users." def list require_application_and_environment! say say "#{'Name'.ljust(14)} #{'Email'.ljust(32)} Updated At" say "-" * 80 User.all.each do |user| say "#{user.name.ljust(14)} #{user.email.ljust(32)} #{user.updated_at.iso8601}" end end end end end
user_command.rb
では、ユーザー一覧を表示するlist
というコマンドを定義しています。このコマンドはbin/rails sample_app:user list
と実行することを意図しています。
list
の先頭でrequire_application_and_environment!
を呼び出し Rails アプリケーションを初期化し、続いて User をデータベースから読み込み、コンソールに出力しています。
なお、Rails アプリケーションの初期化後は、オートロードが効くようになるので、User モデルを明示的に読み込む必要はありません。
micropost
サブコマンドの実体micropost_command.rb
を次のように実装します。lib/commands/micropost/micropost_command.rbmodule SampleApp module Command class MicropostCommand < Rails::Command::Base desc "list", "list microposts." def list require_application_and_environment! say say "#{'Name'.ljust(14)} #{'Content'.ljust(14)} Created At" say "-" * 80 Micropost.all.each do |post| say "#{post.user.name.ljust(14)} #{post.content.ljust(14)} #{post.created_at.iso8601}" end end end end endほぼ
user_command.rb
と同じで、こちらの方は Micropost モデルの一覧をコンソールに出力しています。サブコマンドを追加できたら試しに実行してみます。
$ bin/rails sample_app:user list Name Email Updated At -------------------------------------------------------------------------------- sample sample@example.jp 2020-12-05T05:33:11ZRails チュートリアルを少し進め、ユーザーを登録したら、上のように出力されます。
rails コマンドのその他の実行方法
単に
rails
と実行した場合もbundle exec rails
などと実行した場合もbin/rails
ファイルが実行されますので、bin/rails sample_app:user list
に代えてbundle exec rails sample_app:user list
と実行することもできます。要検討・改善点など
- Rails の作法にならって
Rails::Command::Base
を継承したApplicationCommand
というクラスを作成し、ApplicationCommand
クラスを継承するようにした方が良いのかも?help
コマンドがなからず追加されるが、help
コマンドを実行するとエラーになる。例bin/rails sample_app:help
やbin/rails sample_app:user help
など。
- 理由は標準の
help
コマンドが Rails エンジンしかサポートしてない。Rails アプリケーションは全く考慮されていない。- 改善方法としては
ApplicationCommand
クラスでhelp
コマンドを独自実装するのが良いのかなと考えています。
- 投稿日:2020-12-08T17:55:02+09:00
[Ruby] 自身を実行している処理系の種類を判定する
Ruby という言語には複数の実装があるが、それらをスクリプト上からどのようにして programmatically に見分ければよいだろうか。
Object
クラスに定義されているRUBY_ENGINE
という定数がこの用途に使える。上記ページの例から引用する:
$ ruby-1.9.1 -ve 'p RUBY_ENGINE' ruby 1.9.1p0 (2009-03-04 revision 22762) [x86_64-linux] "ruby" $ jruby -ve 'p RUBY_ENGINE' jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) [i386-java] "jruby"それぞれの処理系がどのような値を返すかだが、stack overflow に良い質問と回答があった。
What values for RUBY_ENGINE correspond to which Ruby implementations? より引用:
RUBY_ENGINE Implementation <undefined> MRI < 1.9 'ruby' MRI >= 1.9 or REE 'jruby' JRuby 'macruby' MacRuby 'rbx' Rubinius 'maglev' MagLev 'ironruby' IronRuby 'cardinal' Cardinal なお、この質問・回答は 2014年になされたものであり、値は変わっている可能性がある。MRI (aka CRuby) については執筆時現在 (2020/12/8) も
'ruby'
が返ってくることを確認済み。この表にない主要な処理系として、mruby は
'mruby'
を返す。mruby 該当部分のソース より引用:
/* * Ruby engine. */ #define MRUBY_RUBY_ENGINE "mruby"
- 投稿日:2020-12-08T16:39:05+09:00
【個人開発】SOFT SKILLSで紹介されているタスク管理法に特化したWebアプリを作ってみた
Rails1ヶ月チャレンジ 1つ目:PomoTask (タスク管理ツール)
※Railsの勉強として、1ヶ月に1個アプリを作っています
作ったWebアプリのリンク:https://pomo-task.herokuapp.com/
はじめに
SOFT SKILLSで紹介されていた時間管理法をご存知ですか?
このタスク管理法はポモドーロテクニック、カンバンなどを融合していて、自宅での作業が効率的に行えるようになっています。
とても良い方法ですが、不満な点が1つだけありました。それがツールの使いやすさです。SOFT SKILLSではKanban Flowが紹介されていましたが、このサービスでは個人の時間管理に特化しているわけではないので、少し不満が出てきます。
- 集団での利用を想定して作られているため、個人では使いづらい
- ポモドーロタイマーが使いづらい
- タスクの締め切りを把握しにくい
- 全体のデザインは硬め
この点を解決すべく他のサービスを探していましたが、自分の好みに合うものがなかなか見つかりませんでした。なので自分で作ることに決めました。
また、前からRailsを扱えるようになりたいと思っていたということもあります。毎月新しいプロジェクトを作ると良いと聞いたので、その1つ目です。RubyもRailsも始めて1ヶ月なので至らない点が多いと思いますが、どうかご覧いただければ幸いです。
作ったもの
先日、PomoTaskというWebアプリをリリースしました。
https://pomo-task.herokuapp.com/
推しポイント
- ポモドーロタイマーの使い心地
- START, SKIP, STOPのみの簡単な操作性
- 何回目かが分かる
- 音とブラウザ通知で開始、終了1分前、終了をお知らせ
- 目標
- その日、週のポモドーロ数が分かる
- 年、四半期、月、週の目標を確認できる
- 週目標は常に見られる
- 締め切りの見やすさ
- 曜日ルーチン
- 毎週行うタスクは自動で追加できる
- 集中力を高めるための仕掛け
- ポモドーロタイマーの色が変わる
- 集中力を高めるためのコツをヘルプに掲載
- 色、視認性
- タスクの背景色
おわりに
最低限使えるレベルのものが作れたような気がします。とりあえず今後は、新しいWebアプリを作ったり、PomoTaskに機能を追加しようと考えています。
付けるべき機能や改善した方がいい点があれば、コメントしていただけると嬉しいです。
参考資料
- SOFT SKILLS ソフトウェア開発者の人生マニュアル - ジョン・ソンメズ
- 自分を操る超集中力 - メンタリストDaiGo
- どんな仕事も「25分+5分」で結果が出る ポモドーロ・テクニック入門 - フランチェスコ・シリロ (勉強に使った参考書:現場で使える Ruby on Rails 5速習実践ガイド)
- 投稿日:2020-12-08T16:34:46+09:00
docker ruby(2.6.5)・mysql(5.6.47)・rails(6.0.0)の開発環境構築
概要
dockerを開発環境に使ってアプリを作成しました。初めてdockerを使ってなかなかうまく動かなかったので動いたものをアウトプットを含めて共有します。また、deviseの導入も行って開発も少々行っていきたいと思います。
dockerの導入
ベースはdockerの公式サイトにRailsとPostgresSQLのdocker-composeの使い方が乗っていたのでそちらを参考にします。
dockerの公式サイト
https://docs.docker.com/compose/rails/ただ、rails6.0.0からはwebpackerが標準になったことにより6.0.0を使う際には修正が必要です。また、今回はmysqlを使うのでそちらも変更していきます。
アプリのディレクトリ作成
ターミナル.mkdir sampleappアプリのディレクトリに移動
ターミナル.cd sampleappdockerfileの生成
ターミナル.touch dockerfiledockerfileの編集
作成したdockerfileを以下のように編集します。
dockerfile.FROM ruby:2.6.5 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs yarn RUN mkdir /myapp WORKDIR /myapp COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock RUN bundle install COPY . /myapp COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"]公式の以下の点を修正しました。
1.rubyのバージョンを変更。
2.webpackerが標準になったことにより、必要になったyarnのインストールを行う。FROM ruby:2.6.5
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.listRUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs yarn
entrypoint.shの生成
touch entrypoint.shentrypoint.shの編集
作成したentrypoint.shを編集します。こちらはdockerの公式サイトの通りになります。
entrypoint.sh#!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@"docker-compose.ymlの生成
touch docker-compose.ymldocker-compose.ymlの編集
生成したファイルを以下のように編集します。
docker-compose.ymlversion: "3" services: db: image: mysql:5.6.47 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: root ports: - "3306:3306" volumes: - ./db/mysql/volumes:/var/lib/mysql web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" stdin_open: true tty: true volumes: - .:/myapp - gem_data:/usr/local/bundle ports: - "3000:3000" depends_on: - db volumes: mysql_data: gem_data:公式のリファレンスではpostgresqlを使っているためmysqlにしています。
services:
db:
image: mysql:5.6.47
environment:
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: password
ports:
- "3306:3306"
volumes:
- ./db/mysql/volumes:/var/lib/mysqlアプリ製作中にbinding.pryを使えるように以下を追加しています。
stdin_open: true
tty: trueデータとgemを永続化するために以下の記述を追加しています。
volumes:
mysql_data:
gem_data:Gemfileの生成
ターミナル.touch Gemfile生成したファイルを以下のように編集します。
Gemfile.source 'https://rubygems.org' gem 'rails', '~>6'空のGemfile.lockを生成
ターミナル.touch Gemfile.lockrailsのプロジェクトを作成
ターミナル.docker-compose run web rails new . --force -d mysqldocker-composeをbuildする
ターミナル.docker-compose builddatabase.ymlの修正
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: dbdocker-composeの再起動
docker-composeを一旦downさせてバックグラウンドで起動するようにコマンドを入力
ターミナル.docker-compose down docker-compose up -dデータベースの作成
ターミナル.docker-compose run web bundle exec rails db:createここまできたらlocalhost:3000/にアクセスしてみましょう。以下の画面が表示されるはずです。
deviseを用いた簡単なアプリの開発
ここからはアプリ開発の入り口をやっていきます。
コントローラーとビューを生成
トップページを表示するためにposts(投稿)コントローラーとindexのビューを生成します
ターミナル.docker-compose run web rails g controller posts index生成したらルーティングを設定します。routes.rbを以下のように編集しましょう。
config/routes.rbRails.application.routes.draw do root 'posts#index' get 'posts/index' endこの時点でlocalhost:3000/にアクセスすると以下のような画面になっています。
先ほど作成したpostsのindex(view)が表示されていることがわかります。deviseの導入
gemファイルの一番したにdeviseを記述します。
Gemfile.gem 'devise'bundle installを行う
dockerを利用している場合には通常行っているコマンドに "docker-compose run web"をつける必要があります。
ターミナル.docker-compose run web bundle installdockerの再起動
gemを新たに追加したためdocker-composeの再起動を行います。
ターミナル.docker-compose down docker-compose up -dあとは通常のdeviseと同じ作業を行っていきます。
それぞれのコマンドを入力していきます。ターミナル.docker-compose run web rails g devise:install docker-compose run web rails g devise user docker-compose run web rails db:migrate docker-compose run web rails g devise:viewsここまできたら再度dockerを再起動させます
ターミナル.docker-compose down docker-compose up -dここまででdeviseの導入全てが終わったためあとはposts/index.html.erbに以下を記述してみましょう。
posts/index.html.erb<%=link_to "ログイン", new_user_session_path %>ここまででlocalhost:3000/にアクセスするとログインボタンが表示され、押すとログイン画面に遷移します。
これでdockerを使ってdeviseを動かすところまで行えました。
ここからはそれぞれのオリジナルアプリの仕様に従って開発ができると思います。
トップページ
- 投稿日:2020-12-08T16:06:58+09:00
[Rails]複数モデルの検索機能
はじめに
検索機能をつけていきたいと思います。
簡易的なものですがポートフォリオ作成等の参考にしてください。今回は検索対象をユーザーか投稿かを選べるようにする且、検索方法を完全一致・部分一致から選択できるようにしたいと思います。(前方一致・後方一致もアレンジで付け加えられるように少しだけ説明します。)
また検索結果を一覧にして新しいページに表示させます。尚、gem ransackは使いません。
こんな人に向けて
1.検索機能の大枠を実装したい人。
2.gem ransackを使いたくない人。
3.実装方法のイメージが湧かない初学者。1.実装をはじめる前に
まずどのように検索していくかの手順を先に説明していきます。
①検索フォームで入力内容を受け取り、コントローラに送る
②コントローラとモデルがデータベースからデータを受け取る。
③ビューに表示させる。簡単にまとめると上記のような流れです。
またこれから実装するにあたって
・if-else文
・whereメソッド
の2点は基本であり重要でもあります。
分からない人は参考になるサイトがネットにたくさんあるので、そちらを見ながら進めてください。2.検索機能の実装
2-1.コントローラーの作成
それでは実装していきたいと思います。
まずコントローラーを作成します。コントローラ名とアクション名は分かりやすければ何でもOKです。(searchやfinder等)
今回はFindersコントローラにfinderアクションをつくります。ターミナルrails g controller Finders finderこれでコントローラ・アクション・ビューが自動作成されました。
2-2.ルートの設定
検索ボタンが押された際にどこのコントローラの何のアクションにリクエストが飛ぶのか設定します。
尚、finderアクションを使っているためresourcesは使用できません。config/routes.rbget 'finder' => "finders#finder"ターミナルで
rails routes
と入力してみましょう。
finder GET /finder(.:format) finders#finder
と出てくるはずです。
これでfinder_path
でリクエストされた際に、findersコントローラのfinder
アクションに飛ぶようになりました。2-3.検索フォームの作成
アクションの中身を書く前に検索フォームから作成します。
form_withを使って入力された内容を受け取っていきます。
検索フォームを設置するページは人によって異なるので、ひとまずはapplication.html.erb
に書きます。
別のページに設置したとしても後の記述は変わりません。app/views/layouts/application.html.erb<%= form_with url: finder_path, method: :get, local: :true do |f| %> <%= f.select :range, options_for_select([['User'], ['Post']]) %> <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %> <%= f.text_field :word %> <%= f.submit "検索" %> <% end %>分解して説明します。
<%= form_with url: finder_path, method: :get, local: :true do |f| %> //findsコントローラのfindアクションに送る情報 <% end %>
form_with ~ do |f|
は入力内容を受け取る定型文です。
最後のf
の部分はform
でもa
でも大丈夫ですが、多くの人はf
を使っています。
url: finder_path
で受け取る情報をどこに送るかを指定しています。
method: :get
はgetメソッドを使うことを宣言しています。
rails routes
にfinder GET /finder(.:format) finders#finder
とあるようにGETメソッドが指定されています。
local: :true
同期通信なのでこのように書きます。<%= f.select :range, options_for_select([['User'], ['Post']]) %> <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>
f.select :range
:選択されたものをrangeとしてアクションに送るようにしています。
f.select
はdo |f|
の記述によって変わり、do |form|
とするならばform.select
とする必要があります。
options_for_select( [ ['要素1'],['要素2'] ] )
:これでタブが作成されます。
要素1・要素2は文字列として扱うのでシングル(ダブル)クォーテーションで囲んでください。
options_for_select( [ ['要素1','A'], ['要素2','B'] ] )
とすることも可能で、要素1とAは同じものとして扱われます。タブには最初の要素1・要素2が表示されます。
また2つの要素だけでなくもっと増やしたい場合は
( [ ['要素1'],['要素2'],['要素3'],['要素4']・・・ ] )
としてください。<%= f.text_field :word %>入力されたものをwordとしてアクションに送るようにしています。
<%= f.submit "検索" %>入力結果を送信します。
"検索"
でボタン内の文字を変えています。検索フォームは完成です。
2-4.モデルの追記
finderアクションで使用する、searchesとwordsの引数を受け取るlooksメソッドをモデルに作成します。
app/models/user.rbdef self.looks(searches, words) if searches == "perfect_match" @user = User.where("name LIKE ?", "#{words}") else @user = User.where("name LIKE ?", "%#{words}%") end endまず条件分岐させ検索方法を変えます。
またwhereメソッドを使いデータベースから該当するのものを全て受け取り@user
に保管します。2-3で説明したように要素を増やし、条件分岐を加え、
"#{words}"
の部分を書き換えることで前方一致や後方一致の検索もできます。2-5.アクションの記述
app/controllers/finders_controller.rbdef finder @range = params[:range] search = params[:search] word = params[:word] @users = User.looks(search, word) end先ほどのフォームで入力された情報をここで受け取ります。
@range = params[:range]
、search = params[:search]
、word = params[:word]
:それぞれ検索フォームで選択・入力された情報を変数に代入しています。後にビューでも使うので@range
と@users
はインスタンス変数にしています。@users = User.looks(search, word)2-4で作ったlooksメソッドを使い、検索結果を
@users
に代入しています。フォームで選択された検索方法は
f.select :search
→params[:search]
→search
→User.looks(search, word)
→def self.looks(searches, words)
→if searches == "perfect_match"
と送られていることになります。
params[:search]
→search
→User.looks(search, word)
の部分をまとめてapp/controllers/finders_controller.rbdef finder @range = params[:range] @users = User.looks(params[:search], params[:word]) endとすることもできます。上記の書き方で進めていきます。
2-6.アクション内での条件分岐
2-5のままではユーザーの検索しかできません。
なのでif文を使い、ユーザーか投稿かを切り替えられるようにします。app/controllers/finders_controller.rbdef finder @range = params[:range] if @range == "User" @users = User.looks(params[:search], params[:word]) else @posts = Post.looks(params[:search], params[:word]) end end分解して説明します。
if @range == "User" //ユーザーから探す処理(user.rbのlooksメソッドを使用) else //投稿から探す処理(post.rbのlooksメソッドを使用) end
@range
にはUserかPostが入っています。
それをif-else文で分けて各モデルのlooksメソッドを使います。2-5で
user.rb
に作成したlooksメソッドを一部書き換えてapp/models/post.rb
にも記述します。app/models/post.rbdef self.looks(searches, words) if searches == "perfect_match" @post = Post.where("name LIKE ?", "#{words}") else @post = Post.where("name LIKE ?", "%#{words}%") end endこれで検索対象も切り替えられるようになりました。
最後にビューを作成して完成です。
2-7.ビューの作成
検索結果一覧を表示するページをつくります。
既存のページに表示することもできますが、今回は新しくページを作成します。app/views/finders/finder.html.erb<% if @range == "User" %> <% @users.each do |user| %> <%= user.name %> //例(ユーザーの名前を表示) <% end %> <% else %> <% @posts.each do |post| %> <%= post.title %> //例(投稿のタイトルを表示) <%= post.text %> //例(投稿の本文を表示) <% end %> <% end %>分解して説明します。
<% if @range == "User" %> //検索対象がUserのとき、ユーザーを一覧表示 <% else %> //検索対象がPostのとき、投稿を一覧表示 <% end %>アクションと同じようにif-else文で条件分岐させています。
<% @users.each do |user| %> <%= user.name %> //例(ユーザーの名前を表示) <% end %> -------------------------------------------------- <% @posts.each do |post| %> <%= post.title %> //例(投稿のタイトルを表示) <%= post.text %> //例(投稿の本文を表示) <% end %>finderアクションでそれぞれ変数定義しましたが、
@users
@posts
には検索に該当するデータが全て含まれています。
それをeach文で繰り返し表示させるよう指示しています。これで完成です。
3.最後に
今回は検索機能の大枠をつくりました。これを活かして細かい部分はアレンジができます。
ぜひ試してください。また実装方法はいろいろありますので、当記事だけでなく他の記事も参考にしてみてください。
- 投稿日:2020-12-08T16:06:58+09:00
検索方法・検索対象を選択できる検索機能を実装する方法(ransack未使用)
はじめに
検索機能をつけていきたいと思います。
簡易的なものですがポートフォリオ作成等の参考にしてください。今回は検索対象をユーザーか投稿かを選べるようにする且、検索方法を完全一致・部分一致から選択できるようにしたいと思います。(前方一致・後方一致もアレンジで付け加えられるように少しだけ説明します。)
また検索結果を一覧にして新しいページに表示させます。尚、gem ransackは使いません。
こんな人に向けて
1.検索機能の大枠を実装したい人。
2.gem ransackを使いたくない人。
3.実装方法のイメージが湧かない初学者。1.実装をはじめる前に
まずどのように検索していくかの手順を先に説明していきます。
①検索フォームで入力内容を受け取り、コントローラに送る
②コントローラとモデルがデータベースからデータを受け取る。
③ビューに表示させる。簡単にまとめると上記のような流れです。
またこれから実装するにあたって
・if-else文
・whereメソッド
の2点は基本であり重要でもあります。
分からない人は参考になるサイトがネットにたくさんあるので、そちらを見ながら進めてください。2.検索機能の実装
2-1.コントローラーの作成
それでは実装していきたいと思います。
まずコントローラーを作成します。コントローラ名とアクション名は分かりやすければ何でもOKです。(searchやfinder等)
今回はFindersコントローラにfinderアクションをつくります。ターミナルrails g controller Finders finderこれでコントローラ・アクション・ビューが自動作成されました。
2-2.ルートの設定
検索ボタンが押された際にどこのコントローラの何のアクションにリクエストが飛ぶのか設定します。
尚、finderアクションを使っているためresourcesは使用できません。config/routes.rbget 'finder' => "finders#finder"ターミナルで
rails routes
と入力してみましょう。
finder GET /finder(.:format) finders#finder
と出てくるはずです。
これでfinder_path
でリクエストされた際に、findersコントローラのfinder
アクションに飛ぶようになりました。2-3.検索フォームの作成
アクションの中身を書く前に検索フォームから作成します。
form_withを使って入力された内容を受け取っていきます。
検索フォームを設置するページは人によって異なるので、ひとまずはapplication.html.erb
に書きます。
別のページに設置したとしても後の記述は変わりません。app/views/layouts/application.html.erb<%= form_with url: finder_path, method: :get, local: :true do |f| %> <%= f.select :range, options_for_select([['User'], ['Post']]) %> <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %> <%= f.text_field :word %> <%= f.submit "検索" %> <% end %>分解して説明します。
<%= form_with url: finder_path, method: :get, local: :true do |f| %> //findsコントローラのfindアクションに送る情報 <% end %>
form_with ~ do |f|
は入力内容を受け取る定型文です。
最後のf
の部分はform
でもa
でも大丈夫ですが、多くの人はf
を使っています。
url: finder_path
で受け取る情報をどこに送るかを指定しています。
method: :get
はgetメソッドを使うことを宣言しています。
rails routes
にfinder GET /finder(.:format) finders#finder
とあるようにGETメソッドが指定されています。
local: :true
同期通信なのでこのように書きます。<%= f.select :range, options_for_select([['User'], ['Post']]) %> <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>
f.select :range
:選択されたものをrangeとしてアクションに送るようにしています。
f.select
はdo |f|
の記述によって変わり、do |form|
とするならばform.select
とする必要があります。
options_for_select( [ ['要素1'],['要素2'] ] )
:これでタブが作成されます。
要素1・要素2は文字列として扱うのでシングル(ダブル)クォーテーションで囲んでください。
options_for_select( [ ['要素1','A'], ['要素2','B'] ] )
とすることも可能で、要素1とAは同じものとして扱われます。タブには最初の要素1・要素2が表示されます。
また2つの要素だけでなくもっと増やしたい場合は
( [ ['要素1'],['要素2'],['要素3'],['要素4']・・・ ] )
としてください。<%= f.text_field :word %>入力されたものをwordとしてアクションに送るようにしています。
<%= f.submit "検索" %>入力結果を送信します。
"検索"
でボタン内の文字を変えています。検索フォームは完成です。
2-4.モデルの追記
finderアクションで使用する、searchesとwordsの引数を受け取るlooksメソッドをモデルに作成します。
app/models/user.rbdef self.looks(searches, words) if searches == "perfect_match" @user = User.where("name LIKE ?", "#{words}") else @user = User.where("name LIKE ?", "%#{words}%") end endまず条件分岐させ検索方法を変えます。
またwhereメソッドを使いデータベースから該当するのものを全て受け取り@user
に保管します。2-3で説明したように要素を増やし、条件分岐を加え、
"#{words}"
の部分を書き換えることで前方一致や後方一致の検索もできます。2-5.アクションの記述
app/controllers/finders_controller.rbdef finder @range = params[:range] search = params[:search] word = params[:word] @users = User.looks(search, word) end先ほどのフォームで入力された情報をここで受け取ります。
@range = params[:range]
、search = params[:search]
、word = params[:word]
:それぞれ検索フォームで選択・入力された情報を変数に代入しています。後にビューでも使うので@range
と@users
はインスタンス変数にしています。@users = User.looks(search, word)2-4で作ったlooksメソッドを使い、検索結果を
@users
に代入しています。フォームで選択された検索方法は
f.select :search
→params[:search]
→search
→User.looks(search, word)
→def self.looks(searches, words)
→if searches == "perfect_match"
と送られていることになります。
params[:search]
→search
→User.looks(search, word)
の部分をまとめてapp/controllers/finders_controller.rbdef finder @range = params[:range] @users = User.looks(params[:search], params[:word]) endとすることもできます。上記の書き方で進めていきます。
2-6.アクション内での条件分岐
2-5のままではユーザーの検索しかできません。
なのでif文を使い、ユーザーか投稿かを切り替えられるようにします。app/controllers/finders_controller.rbdef finder @range = params[:range] if @range == "User" @users = User.looks(params[:search], params[:word]) else @posts = Post.looks(params[:search], params[:word]) end end分解して説明します。
if @range == "User" //ユーザーから探す処理(user.rbのlooksメソッドを使用) else //投稿から探す処理(post.rbのlooksメソッドを使用) end
@range
にはUserかPostが入っています。
それをif-else文で分けて各モデルのlooksメソッドを使います。2-5で
user.rb
に作成したlooksメソッドを一部書き換えてapp/models/post.rb
にも記述します。app/models/post.rbdef self.looks(searches, words) if searches == "perfect_match" @post = Post.where("name LIKE ?", "#{words}") else @post = Post.where("name LIKE ?", "%#{words}%") end endこれで検索対象も切り替えられるようになりました。
最後にビューを作成して完成です。
2-7.ビューの作成
検索結果一覧を表示するページをつくります。
既存のページに表示することもできますが、今回は新しくページを作成します。app/views/finders/finder.html.erb<% if @range == "User" %> <% @users.each do |user| %> <%= user.name %> //例(ユーザーの名前を表示) <% end %> <% else %> <% @posts.each do |post| %> <%= post.title %> //例(投稿のタイトルを表示) <%= post.text %> //例(投稿の本文を表示) <% end %> <% end %>分解して説明します。
<% if @range == "User" %> //検索対象がUserのとき、ユーザーを一覧表示 <% else %> //検索対象がPostのとき、投稿を一覧表示 <% end %>アクションと同じようにif-else文で条件分岐させています。
<% @users.each do |user| %> <%= user.name %> //例(ユーザーの名前を表示) <% end %> -------------------------------------------------- <% @posts.each do |post| %> <%= post.title %> //例(投稿のタイトルを表示) <%= post.text %> //例(投稿の本文を表示) <% end %>finderアクションでそれぞれ変数定義しましたが、
@users
@posts
には検索に該当するデータが全て含まれています。
それをeach文で繰り返し表示させるよう指示しています。これで完成です。
3.最後に
今回は検索機能の大枠をつくりました。これを活かして細かい部分はアレンジができます。
ぜひ試してください。また実装方法はいろいろありますので、当記事だけでなく他の記事も参考にしてみてください。
- 投稿日:2020-12-08T15:54:40+09:00
Gem 'Gimei'
Gimeiとは
日本人の名前やフリガナ、住所などを自動生成してくれるGem。
有名なGemでFakerがあるが、Fakerでは対応できないフリガナを使うことができる。
Gimei使い方
開発環境とテスト環境で利用するのでgroup :development, :test doの内部でgemを指定、Gemfileを編集したらアプリケーションのディレクトリでbundle installを実行。
Gemfilegroup :development, :test do # 省略 gem 'rspec-rails' gem 'factory_bot_rails' gem 'faker' gem 'gimei' endコンソールで実行するとこんな感じで名前が生成される。
console[1] pry(main)> japanese_user = Gimei.name # 省略 [2] pry(main)> japanese_user.last.kanji => "島村" [3] pry(main)> japanese_user.last.katakana => "シマムラ"FactoryBotと組み合わせて架空のユーザーを生成する。
インスタンスを生成せずにGimei.name.first.kanji
などを入れると、名前とフリガナが一致しなくなる。factories/users.rbFactoryBot.define do factory :user do # インスタンスを生成 japanese_user = Gimei.name first_name { japanese_user.first.kanji } first_name_kana { japanese_user.first.katakana } last_name { japanese_user.last.kanji } last_name_kana { japanese_user.last.katakana } end endconsole[1] pry(main)> FactoryBot.create(:user) # 以下実行結果が表示される。
- 投稿日:2020-12-08T15:27:05+09:00
【Rails】表の合計値算出方法(aggregate関数)
はじめに
Railsで表の合計値を算出するにあたって、aggregate関数なるものが便利でスマートだったので、記事にしてみました。
開発環境
IDE:Cloud9
Ruby:2.6.3
Rails:5.2.4実例をみてみる
やりたいこと
ER図
aggregate関数を使わない場合
controllers/carts.rbdef new @carts = Cart.where(user_id: current_user.id) end例えば、たんぱく質(protain)の合計値を表示したい場合
views/carts/new.html.rb<% sum = 0 %> <% @carts.each do |cart| %> <% sum += cart.food.protain %> <% end %> <%= sum %>これだと、1つの項目(上記の場合たんぱく質)を表示するのに、5行も必要となってしまい、見ため的にあまりスマートとはいえない。
views/carts/new.html.rb<td>合計</td> <td> <% sum = 0 %> <% @carts.each do |cart| %> <% sum += cart.food.calorie %> <% end %> <%= sum %> </td> <td> <% sum = 0 %> <% @carts.each do |cart| %> <% sum += cart.food.protain %> <% end %> <%= sum %> </td> <td> <% sum = 0 %> <% @carts.each do |cart| %> <% sum += cart.food.fat %> <% end %> <%= sum %> </td> <td> <% sum = 0 %> <% @carts.each do |cart| %> <% sum += cart.food.carbon %> <% end %> <%= sum %> </td>表全体を表示すると、割とfat感がある。
そこでaggregate関数を用いて、もっとスマートに記述する。aggregate関数を使う方法
コントローラはさっきと一緒。
controllers/carts.rbdef new @carts = Cart.where(user_id: current_user.id) endカートモデルに以下を記述する。
model/cart.rbdef self.aggregate(column) self.all.map { |cart| cart.food[column] }.sum endviews/carts/new.html.rb<td>合計</td> <td> <%= @carts.aggregate(:calorie) %> </td> <td> <%= @carts.aggregate(:protain) %> </td> <td> <%= @carts.aggregate(:fat) %> </td> <td> <%= @carts.aggregate(:carbon) %> </td>めっちゃすまーと。
- 投稿日:2020-12-08T14:40:56+09:00
RSpec 独自バリデーションのテストで少しハマった話
はじめに
RSpecのテストを記述中
size
メソッドとcount
メソッドの違いを知らず少しハマったのでメモして行きます。
Rails 6.0.3.4
ruby 2.6.3p62
RSpec 3.10
テスト内容
親モデルのuserは子モデルhabitを複数登録できる 1対多の関係。
そこでuser
はhabit
モデルを6つまでしか登録できないという独自のバリデーションを加えてあります。
habit.rb
class Habit < ApplicationRecord belongs_to :user validates :task, presence: true, length: { maximum: 12 }, uniqueness: { scope: :user } validates :frequency, presence: true validate :user_habits_size_validate HABIT_MAX = 6 def user_habits_size_validate if self.user && self.user.habits.size >= HABIT_MAX errors.add(:task,"は#{HABIT_MAX}つまでしか登録出来ません") end end endこのバリデーションが正常に機能してるかのテストを書こう思い、このように書いてみました。
(FactroyBotでuser
とhabit
は作成済み)
habit_spec.rb
it 'userはhabitモデルを6つ以上持てない' do habits = create_list(:habit, 6, user: user) expect(build(:habit, task: "筋トレ", user: user).save).to be_falsey end
create_list
でhabit
を6つ作成し、7つ目ではfalse
が返る。
これで実行してみると1) Habit バリデーション userはhabitモデルを6つ以上持てない Failure/Error: expect(build(:habit, task: "筋トレ", user: user).save).to be_falsey expected: falsey value got: truetrueが返ってきた
原因は
habit.rb
のsize
メソッドにありました。
self.user.habits.size >= HABIT_MAX
この
size
メソッドはキャッシュを参照している。つまり
self.user.habits.size >= HABIT_MAX
この式は常にfalse
になるのでいくらhabit
を生成してもバリデーションはかからなかった。なので,
count
メソッドを使いました。count
は常にSQLを発行して確認する。
self.user.habits.count >= HABIT_MAX
これでテストを走らせると
..... Finished in 0.40233 seconds (files took 1.41 seconds to load) 5 examples, 0 failures無事通りました。
ちなみにこうしてもテストは通りました。↓
self.user.habits.reload.size >= HABIT_MAX
..... Finished in 0.41723 seconds (files took 1 .43 seconds to load) 5 examples, 0 failures最後に
書いていて気づいたんですが、そもそもテスト通すために
habit.rb
の方をいじるのは違くないか?と思い、また試行錯誤中です。まだまだ勉強中ですので間違いなどありましたらご指摘いただけると幸いですm(__)m
- 投稿日:2020-12-08T12:49:46+09:00
【初心者】Rubyでbinding.pryを使う方法【絶対使うべき】
概要
勉強会で、Rubyで「binding.pry」を使えるようにする方法を学びましたので、まとめました。
設定方法
①Gemfileを作成
ターミナルtouch Gemfile
Railsではなく、rubyのフォルダにGemfileを作るという考え方がなかったので、驚きました。
Github:https://github.com/pry/pry②Gemfileを記述
githubを参考に、コピペします
Gemfilegem 'pry', '~> 0.13.1'③インストール
ターミナルbundle install
④設定
main.rb#pryを読み込みたいファイルに記述する require'pry'⑤使い方
main.rb#止めたいところに記述 binding.pry上記を入力してある状態でコードを実行すると
「binding.pryを記述したところ」でとまるので、以下のようなことを試してください。
- 変数に何が格納されているか?
- 期待している値は入っているか?
- binding.pryの部分で止まるのか?そうでない部分でエラーが出るか?
以上になります。
まとめ
binding.pryを使えるようになって、putsやpに出力させる必要がなくなり、開発効率が上がりました。
難しそうと思って使っていなかった過去の自分を叱りたいです。
使ったことがない方は使ってみてください。
- 投稿日:2020-12-08T11:07:39+09:00
sliceメソッドを使ったAPI問題
本日はこちらの問題を解いていきます。
問題.
任意の文字列の最後の2文字を3回繰り返し
出力するメソッドを作りましょう。出力例:
extra_end('Hello') → 'lololo'
extra_end('ab') → 'ababab'
extra_end('Hi') → 'HiHiHi'ヒント
sliceメソッドを用いることで、配列や文字列から指定した要素を取り出すことができます。# 配列を作成します array = [0,1,2,3,4,5,6] # 配列から引数で指定した要素をsliceします ele1 = array.slice(1) puts ele1 #=> 1 # 配列番号1から4つ分の要素をsliceします ele2 = array.slice(1,4) puts ele2 #=> 1 2 3 4 # 配列はもとのままです puts array #=> [0,1,2,3,4,5,6]模範回答
def extra_end(str) char_num = str.length right2 = str.slice(char_num - 2, 2) puts right2 * 3 end解説
たとえば、extra_end('Hello')でメソッドを呼び出した場合、
.lengthを使うことによって1から数えることが出来ます。(使わないと0から数えてしまいます。)
char_num = 5となり、right2 = str.slice(3,2)になります。
slice(3,2)は配列番号(インデックス)3つ目から数えて2つ分の要素を切り取ります。
今回の場合、切り取られた結果loが残り、right2 = loとなります。
最終的に、right2 * 3、つまりlololoと出力されます。
- 投稿日:2020-12-08T10:03:27+09:00
OpenAPIのYAML分割管理と構成案
はじめに
READYFORのエンジニアリング部に所属している熊谷です。
この記事はREADYFOR Advent Calendar 2020の8日目の記事です。
概要
スキーマー駆動開発でOpenAPI(旧Swagger)を導入し始めたところなのですが、その中で、OpenAPIの運用管理について色々調査・検討していたので、記事として共有させていただきます。
対象読者
以下の方々を対象としています
- API開発でOpenAPI導入を検討している方。
- 既にOpenAPIの導入済みの方。
背景 ( 課題感 )
スキーマー駆動開発でOpenAPI(旧Swagger)を採用している企業は多いかと思いますが、OpenAPI導入において最初に感じた課題感として、陥りそうな状況の一つとして、最初に運用方針を決めないまま、多数のメンバーが一つのopenapi.yamlにスキーマー定義を追加・更新していった場合、
- ファイルサイズが膨れあがり、
- スキーマー定義も一貫性がなくなり、
- 運用管理が覚束なくなる、
みたいなケースが想定されるのではと思いました。
そのため、予めそのようなサービス拡大にも耐えられるように、また、マイクロサービスなど複数サービスにも対応できるように、OpenAPIの運用方針・構成を考えみました。(一部、実際に運用開始しています)
OpenAPIの構成
OpenAPI専用のGitリポジトリを作成し、下記のような構成で構築します。
( 記事の最後にサンプルgitのリンクを貼っています )構成イメージ
全体のディレクトリ構成のイメージです。
openapi.yamlは、直接編集するのではなく、openapi-generatorを使って中間ファイルから生成するようにします。中間ファイルを用いることでYAMLを分割して定義することができるようになります。
ディレクトリ構成
具体的には、下記のようなディレクトリ構成になります。
./ ├── README.md ├── openapi │ ├── {サービス名} │ │ └── openapi.yaml │ └── api(ex) │ └── openapi.yaml └── src ├── components:全体の共通コンポーネント └── services: └── {サービス名} ├── root.yaml:中間ファイル ├── paths:各エンドポイントのスキーマー定義 └── examples:Example用YAML ├── api(ex) ├── root.yaml:中間ファイル ├── paths:各エンドポイントのスキーマー定義 │ ├── animals │ │ ├── cats.yaml │ │ └── dogs.yaml │ └── fruits │ └── apples.yaml ├── examples:Example用YAML │ ├── animals-cats-example-1.yaml │ ├── animals-dogs-example-1.yaml │ └── fruits-apples-example-1.yaml ├── scripts:各種生成スクリプト群 ├── openapi2generator-ruby.sh ├── root2openapi.sh └── swagger-ui.shディレクトリ概要
各ディレクトリの概要と、そこに配置するYAMLファイル名のフォーマットです。
ディレクトリ名 概要 YAMLファイル名 openapi OpenAPIファイル群
・中間ファイルから自動生成されたファイル群
・直接このファイルは修正することはないopenapi.{サービス名}.yaml src/services 中間ファイル群
・このファイルを元に./openapi/配下のyamlを生成する。{サービス名}.yaml src/services/*/paths スキーマー定義ファイル群
・スキーマー定義が記述されている。
・中間YAMLから参照される。{タグ名}/{エンドポイント名}.yaml src/services/*/paths/*/components タグの共通コンポーネント用ファイル群 {コンポーネント名}.yaml src/services/*/examples Exampleファイル群
(アンダースコアやディレクトリを用いると上手く生成されないためハイフンで繋げる){タグ名}-{エンドポイント名}-example-{No}.yaml src/components 全体の共通コンポーネント用ファイル群
・ページング情報、バリデーション、認証情報など全社的に共通フォーマットと定義した方がいいようなもの。{コンポーネント名}.yaml scripts スクリプトファイル群
・openapi.yamlやRubyコード生成スクリプトなど。各ファイル記述
各YAMLファイルに記述する内容を順に紹介します。
中間ファイル
./src/services/*.yaml
- このディレクトリには中間ファイルを配置します。
- 中間ファイルには、サービス概要とエンドポイント一覧のみを記述します。
- (各エンドポイントのスキーマー定義は記述しません。)
項目 説明 openapi 3.0.0 info openapiの基本情報 servers テストで使用するサーバー情報を記述する tags 各ドメインの概要を記述する 補足:
タグ名=ドメインとして定義する。paths 各エンドポイント一覧を記述する フォーマット: $ref: ./paths/{タグ名}/{エンドポイント名}.yaml
src/services/api.yamlopenapi: 3.0.0 info: title: XXXX API description: "XXXX Service API" version: '1.0' contact: name: XXXX Service API url: 'https://xxx..jp' email: xxx@xxxxx.xx termsOfService: 'https://xxxx.xx/terms' servers: - url: 'http://localhost:3000' description: development ... tags: - name: animals description: 動物 - name: fruits description: 果物 paths: # Animals: 動物 /animals/cats: $ref: ./paths/animals/cats.yaml /animals/dogs: $ref: ./paths/animals/dogs.yaml /animals/dogs: $ref: ./paths/animals/rabbits.yaml /animals/rabbits: ... # Fruits: 果物 /fruits/apples: $ref: ./paths/resources/apples.yaml /fruits/oranges: $ref: ./paths/fruits/oranges.yamlスキーマー定義ファイル
./src/services/{サービス名}/paths/{タグ名}/*.yaml
- 各エンドポイントごとにスキーマー定義を記述します。
- operationIdや各オブジェクト名は、コンフリクトを起こさないように、一貫性を持たせます。
- examplesを同じファイルに纏めると、見通しが悪くなるため、refs参照を使い、別ファイルに分けて管理します。
項目 説明 フォーマット operationId エンドポイントのユニークID {タグ名}_{エンドポイント名}
{タグ名}_{エンドポイント名}_{メソッド名}
・CURDなど複数メソッドに対応する場合summary エンドポイント名のタイトルを記述する - description エンドポイント名の詳細仕様を記述する。
・なるべく丁寧に詳細に記述する。- parameters リクエストのスキーマーを定義する $refs名: {operationId}_Params"
{operationId}_{オブジェクト名}Params"
responses レスポンスのスキーマーを定義する $refs名:
・第一階層 ={operationId}
・第二階層以下 ={operationId}_{オブジェクト名}
responses
.examples
.exampleテストデータのYAMKファイルを指定する $refs:
{タグ名}-{エンドポイント名}-example-{No}.yaml
l
・ハイフンやディレクトリ構成は不可のためハイフンで繋げる。properties プロパティ名 ・ローワーキャメルケースで記述する。(TSの都合上)
・user_id → userIdrequiered 必須項目 必須 /paths/animals/dogs.yamlget: summary: 犬一覧を取得する operationId: Animals_DogsGet description: | xxxxxxxxxxxxxxxx tags: - animals responses: "200": content: application/json: schema: $ref: Animals_DogsGet examples: example_1: $ref: '../../examples/animals-dogs-get-example-1.yaml' example_2: $ref: '../../examples/animals-dogs-get-example-2.yaml' post: summary: 犬一覧を取得する operationId: Animals_DogsPost description: | xxxxxxxxxxxxxxxx tags: - animals responses: "200": content: application/json: schema: $ref: Animals_DogsGet examples: example_1: $ref: '../../examples/animals-dogs-post-example-1.yaml' components: schemas: # Dogs Get Animals_DogsGet_Params: type: object properties: type: type: integer Animals_DogsGet: type: object properties: name: type: string age: type: integer # Dogs Post Animals_DogsPost_Params: type: object properties: name: type: string age: type: integer Animals_DogsPost: type: object properties: result: boolean
- スキーマーオブジェクトには多様なプロパティがあり、表現の自由度も高いため、フロントエンド・バックエンドで最低限必要な項目のみに絞るようにしています。
フィールドタイプ名 説明 type タイプ required 必須 properties.type プロパティの型 properties.description プロパティの概要 properties.nullable properties.enum 共通コンポーネント
./src/components/*.yaml
複数サービスで共通化する必要がある、抽象度の高いオブジェクトをコンポーネントとして記述します。
- ( ファイル名に違和感があるのですが、refs参照の際、ファイル名がそのままオブジェクト名として生成されるため、キャメルケースとしています。)
Common_Image.yamltype: object description: | 画像オブジェクト properties: src: type: string alt: type: string required: - src - altExampleファイル
./src/services/{サービス名}/examples/*.yaml
Exampleをスキーマー定義と同一ファイルにおくと、見通しが悪くなるため、examplesディレクトリを区切り管理します。
- (補足として、各スキーマーのexampleフォーマットは、openapi-generatorで、中間ファイルからopenapi.yamlを生成する際に、自動生成されるので、それを用いるとスムーズです。)
animals-dogs-example-1.yamlvalue: dogs: [ { id: 1, name: taro } ]スクリプト例
主要な部分のみ抜粋してます。
1. 中間ファイル → openapi.yaml
root2openapi.shservice_name=$1 root=${PWD} src=${root}/src/services/${service_name} out=${root}/openapi/${service_name} components=${root}/src/components docker run --rm \ -v "${src}:/local/src/" \ -v "${out}:/local/dist/openapi" \ -v "${components}:/local/src/components" \ openapitools/openapi-generator-cli generate \ -g openapi-yaml \ -i /local/src/root.yaml \ -o /local/dist2. openapi.yaml → rubyシリアライザ
openapi2generator-ruby.shservice_name=$1 root=${PWD} src=${root}/openapi/${service_name}/openapi.yaml out=${root}/dist/openapi2generator-ruby docker run --rm \ -v "${src}:/local/openapi.yaml" \ -v "${out}:/local/dist/openapi2generator-ruby" \ openapitools/openapi-generator-cli generate \ -g ruby \ -i /local/openapi.yaml \ -o /local/dist/openapi2generator-ruby3. Swagger-ui起動
service_name=$1 root=${PWD} openapi=${root}/openapi/${service_name}/ docker run \ -p 80:8080 \ -e SWAGGER_JSON=/src/openapi.yaml \ -v `pwd`/openapi/${service_name}:/src swaggerapi/swagger-ui余談:REST/RPCについて
本題から少し逸れますが、記事の例文では、わかりやすくするためにREST指向のエンドポイントで記述していますが、実際の運用では、REST/RCPの両方を許容する形で運用しています(既存のAPIがRESTというのもありますが)。ただ、混在させると困惑が生じるため、サービス・タグごとにAPI設計する中で、最適な方を採用するという方針としてます。
REST/RPCに関しては、OpenAPIを色々調査する中で、「OpenAPI(旧Swagger)はREST APIを設計するためのツール」と紹介される記事を多く目にしますが、OpenAPI 3.1.0では、下記のように「REST APIs」の表記が全て「HTTP APIs」と書き換わっていることは着目しておく必要はあるかなと思いました。
OpenAPI-Specification | OpenAPI supports any type of plain HTTP API
- language-agnostic interface description for REST APIs + language-agnostic interface description for HTTP APIsまたその中で、stoplightの開発者でもあるphilsturgeon氏が、下記のようにRPCについて言及しており、OpenAPI Initiativeメンバーであるdarrelmiller氏がそれに同意し、v4でのgRPCサポートも示唆されています。
● philsturgeon commented on Jun 12, 2019
Twice in the last few days I have had people ask if its ok to use OpenAPI for RPC, and I would say its better at describing RPC than REST currently.
ここ数日で2回、RPCにOpenAPIを使ってもいいかと聞かれたことがあります。現在のところ REST よりも RPC の記述の方が優れていると言っています。Lets remove the limitation by fixing this wording, which would unblock larger talks about things like gRPC support for v4, and maybe even other level 0 implementations like GraphQL.
この文言を修正することで制限を取り除き、v4のためのgRPCサポートのようなものについての大きな話をブロックしないようにしましょう。● darrelmiller commented on Jun 13, 2019
I do agree that attempting to associate OpenAPI to REST is no longer doing OpenAPI any favours.
OpenAPIをRESTに関連づけようとすることは、もはやOpenAPIのためにならないことに同意します。誤解のないように補足しておくと、ここで言いたいこととしては、REST/RPCのどちらかが優れているのかという話ではなく、サービスの特性に合わせて、最適なAPIを設計をできるように、多くの可能性を選択肢として判断できるようにしておくことが大切だと思いました。
git. openapi-skeleton
今回紹介させていただいたYAMLファイルやスクリプトと置いてあります。
https://github.com/rkumagai/openapi-skeleton
まとめ
OpenAPIの運用に関しては、まだ導入フェーズということもあり、まだ詰め切れてないこともあり、運用しながら試行錯誤しながらブラッシュアップしていく予定です。また、OpenAPI自体の構成・管理方法よりも、実際にどうのようにAPIを設計するのかを考える方が重要で、難しいなと感じています。少しでも参考になれば幸いです。
明日はyamanokuさん記事になります。お楽しみに。
- 投稿日:2020-12-08T09:19:39+09:00
openapi-generatorのadditional-propertiesを指定して生成されるコードをカスタマイズしよう
https://github.com/OpenAPITools/openapi-generator
openapi-generatorはOpenAPIの定義ファイルから、API ClientやServer stubsなどを自動生成してくれる便利なものです。対応言語も豊富なので、これからAPI Clientを作ろうと考えている方は、これを用いて自動生成することをおすすめします。
$ openapi-generator generate -i ./openapi.yml -o ./openapi_client -g ruby上記のコマンドを実行すると、openapi.ymlをもとに、openapi_clientというディレクトリ配下にrubyのコード(実態はgem)が生成されます。
openapi-generatorコマンドには多彩なオプションがあり、以下のコマンドで確認ができます。
$ openapi-generator help generate本記事ではこの中にある
additonal-properties
というオプションについて説明します。additonal-propertiesについて
このオプションは、生成される各言語ごとのオプションを設定できるものです。
https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/ruby.md
例えばRubyのAPI Clientであれば上記のドキュメントにあるものがすべてadditional-propertiesで指定できます。
RubyのAPI Clientはデフォルトではopenapi_client
というgem名で生成されますが、このgemの名前を変えたい場合は下記のようにオプションを指定することで変更ができます。$ openapi-generator generate -i ./openapi.yml -o ./openapi_client -g ruby --additional-properties=gemName=qiita_client最近だと1つのリポジトリから複数のサービスのAPIを呼び出すことも多々あるでしょう。その場合はopenapiの定義ファイルが複数あることになると思いますが、すべて同じopenapi_clientというgemで生成されてしまうとどれか一つのサービスのAPIしか呼び出せなくて困ってしまいます。
そこで上記のオプションを使って別のgemとして生成することで、複数のサービスのAPIを呼び出すことが可能になります。
他にも便利そうなオプションがいくつかありますが、additional-propertisに複数指定したい場合はカンマ区切りでkeyとvalueをつなげると良いようです。詳しくはドキュメントを見てください。言語ごとにオプションがいろいろあるので、すでに自動生成している方も一度ドキュメントを確認してみると良いのではないでしょうか?
それでは良い自動生成生活を!
- 投稿日:2020-12-08T08:21:14+09:00
gemのバージョン指定について
railsアプリケーションを作成するにあたって
gemファイル内のバージョン指定について理解が不十分だったので整理してみました。Gemfileとは
・これは何?
Bundlerというrubyのライブラリ管理システムのファイル。
・何ができる?
railsアプリで使用するライブラリを管理することができる。バージョンの基本の書き方
Gemfilegem 'gem名', 'バージョン', 'オプション'x.y.zの表記の意味
x:メジャーバージョン
重大な変更。新機能の追加や、多くのAPI変更が含まれる。
y:マイナーバージョン
新機能の追加やAPIの追加が含まれることがある。
z:パッチバージョン
バグの修正が含まれる。バージョン指定の指定について
固定
'x.y.z'
Gemfilegem 'sqlite3', '1.3.6'〜以上
'>=x.y.z'
Gemfilegem 'sqlite3', '>=1.3.6'x.x.x以上、x.y+1.0未満(メジャーアップデート不可)
'~>x.y.z'
Gemfilegem 'sqlite3', '~>1.3.6'以下と同義ですね。
Gemfilegem 'sqlite3', '>=1.3.6', '<1.4.0'x.y.z以降で最新のもの
'>=x.x.x'
Gemfilegem 'sqlite3', '>=0.8.5'参考
https://blog.yuhiisk.com/archive/2017/04/24/specify-the-version-of-gemfile.html
https://haayaaa.hatenablog.com/entry/2018/10/29/235952
- 投稿日:2020-12-08T01:37:06+09:00
DeviseのUserテーブルにUpdateアクションでカラムを更新しようとするとうまくいかない現象について
事の発端
Deviseで作ったUserモデルのテーブルにカラムを更新しようとするとできなかったことが始まりです。
意外なところで詰まったなぁと思ったので健忘録としてまとめます。
user_controller.rbdef update @user = User.find(params[:id]) if @user.update(user_params) redirect_to user_path(@user.id) else render :show end end僕は月の走行距離をマイページにて追加したかった為詳細ページにform_withを構えています。
show.html.erb<%= form_with(model: @user, local: true, class: "goal-form") do |f| %> <%= f.text_field :distance, placeholder: "目標を記入する", class: "form__text" %> <%= f.submit "設定する", class: "btn btn-primary" %> <% end %>送られてくる値も間違ってなかったのでなんでやねんと思っていたとこでした。
そこで以下のエラーを見つけました。
そもそもUserテーブルの編集にはPasswordの入力が必要だということ。
知りませんでした。
これまでユーザーの編集を行うことがなかったんです。そこでPasswordを入力せずにユーザーの編集を行う方法を見つけました。
カラムを更新するには新たなコントローラの作成やメソッドが必要なようです。まずは、users/registrations_controller.rbを作成します。
registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController before_action :configure_account_update_params, only: [:update] protected def configure_account_update_params devise_parameter_sanitizer.permit(:account_update, keys: [:name]) end end次にルーティングを設定します。
route.rbdevise_for :users, controllers: { registrations: 'users/registrations' }Userモデルを編集。メソッドを追加します。
user.rbメソッドを追加 def update_without_current_password(params, *options) params.delete(:current_password) if params[:password].blank? && params[:password_confirmation].blank? params.delete(:password) params.delete(:password_confirmation) end result = update_attributes(params, *options) clean_up_passwords result end endUserモデルで定義したメソッドを呼び出します。
registrations_controller.rbclass Users::RegistrationsController < Devise::RegistrationsController before_action :configure_account_update_params, only: [:update] protected 追加 def update_resource(resource, params) resource.update_without_password(params) end def configure_account_update_params devise_parameter_sanitizer.permit(:account_update, keys: [:distance]) endまた、モデルにpasswordのバリデーションがかかっているとまだエラーになると思うので外しておきましょう。
user.rbwith_options presence: true do validates :nickname validates :email validates :password ←消す endhttps://gyazo.com/d2738db841e41a0679d16fae6836ace9
できました。ほぼコピペです…
ここで初見だった方々が多かったのでまとめてみました。
・:account_update・・・Updateをするときに指定する引数。
・blank?・・・中身が空もしくは存在しないときにtrueを返す。
・update_attributes・・・一つのカラムのみを変更できる。しかし、バリデーションがスルーされる為、エラーの判定位が出ない。(これに苦しめられた)キリがないのでここまで。
完成
これから、jsで今日走った距離が引かれて減っていくような機能を導入したいなと思います。
お疲れ様でした。参考文献
https://qiita.com/j-sunaga/items/8d6769dfd04da5d3eed5
https://qiita.com/somewhatgood@github/items/b74107480ee3821784e6
- 投稿日:2020-12-08T01:11:53+09:00