- 投稿日:2022-01-16T23:32:46+09:00
[Rails][devise]追加したカラムにストロングパラメーターを使えるようにしよう
備忘録として ユーザー管理機能を実装できるdevise デフォルトとして「Eメールアドレス」「パスワード」のカラムがすでに存在しており、ストロングパラメーターも使用できるが、自分で追加するカラム(ユーザー名など)にはストロングパラメーターは使えない状態なので、使えるようように記述する必要がある。 前提 deviseを導入済み→deviseの導入手順 マイグレーションファイルでカラムを追加している application_controller.rbファイル すべてのコントローラーが継承しているファイル。 ここに処理を記述しておくことで、すべてのコントローラーで共通となる処理を作ることができる。 deviseの処理を行うコントローラーはGem内に記述されているため編集することができないので、このapplication_controller.rbを編集していく。 devise_parameter_sanitizerメソッド deviseにおけるparamsのようなメソッド。deviseのUserモデルに関わる「ログイン」「新規登録」などのリクエストからパラメーターを取得できる。 このメソッドとpermitメソッドを組み合わせることにより、deviseに定義されているストロングパラメーターに対し、自分で新しく追加したカラムも指定して含めることができる。 普通のストロングパラメーターと同じく、新たに定義するPrivateメソッドの中で使用します。 application_controller.rb private def configure_permitted_parameters # メソッド名は慣習(自由につけても構わない) # deviseのUserモデルにパラメーターを許可 devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])#追加したカラムを記述 end 第一引数の処理名には、deviseですでに設定されているsign_in, sign_up, account_updateが使用できる。 処理名 役割 :sign_in サインイン(ログイン)の処理を行うとき :sign_up サインアップ(新規登録)の処理を行うとき :account_update アカウント情報更新の処理を行うとき 実際に編集してみる application_controller.rb class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname]) end end 解説 2行目 before_actionのifというオプションを使用している点に注目。 これは、値にメソッド名を指定することで、その戻り値がtrueであったときにのみ処理を実行するよう設定している。 今回は:devise_controller? というdeviseのヘルパーメソッドを指定して、もしdeviseに関するコントローラーの処理であれば、そのときだけconfigure_permitted_parametersメソッドを実行するように設定してる。 まとめ 自身で追加したカラムでストロングパラメーターを使えるようにするには、特別な記述が必要 devise_parameter_sanitizerメソッドを使用する application_controller.rb内に記述する ※補足等があればコメントいただけると幸いです。
- 投稿日:2022-01-16T22:48:55+09:00
【Rails × AWS】本番環境のDB(MySQL)をRDSに移行し、タイムゾーンを日本時間に変更する
EC2環境にデプロイしていたRailsアプリのデータベースを後からRDSに移行したので自分自身の復習も兼ねて手順をまとめました。 前提 Rails -v 6.0.4 MySQL -v 5.7.36 サーバー: nginx AWS側の設定 AWSのRDSダッシュボードを開く 「データベースの作成」を選択 エンジンのタイプを選択 現在使用しているSQLの種類を選択。 今回はMySQLを選択します。 4.MySQlのバージョンを選択 現在使用しているMySQLのバージョンと同じものを選択します。 MySQLのバージョンの確認方法は下記の通りです。 [ec2-user]$ mysql -u root -p mysql> select version(); 5.テンプレートを選択 今回は無料枠を利用することとします。 下の方に概算コストが表示されるので高性能なテンプレートを選択する場合はそちらを確認してください。 6.基本情報を設定 いずれの項目も自由に設定できますがマスターユーザー名とパスワードは後で使用するので控えるようにしてください。 7.DBインスタンスのクラスを選択 db.t2.microを選択します。1GBのメモリを搭載、実験に適したサイズです。 8.ストレージを設定 今回は初期設定のままとします。 1年間は無料利用枠の対象となります。 9.可用性と耐久性 無料利用枠のテンプレートを選択した場合こちらは設定できないのでスルーします。 10.接続の設定 VPCとサブネットグループに関しては現在使用しているEC2インスタンスと同じ場所に作成します。 パブリックアクセスはこのDBインスタンスにパブリックIPを割り当てるかどうかを設定するものになります。同じVPCに設置したEC2インスタンスから接続できれば十分なので「いいえ」を選択します。「はい」にすると攻撃されるリスクがあります。 VPCセキュリティグループは新規で作成します。あとで中身は設定します。 アベイラビリティゾーンはEC2インスタンスと同じ場所に配置するので自身のEC2インスタンスのアベイラビリティゾーンを確認して選択してください。 11.データベースの作成を選択する 残りの項目については今回はデフォルトのままでいきます。 12.データベースの完成を待つ DBインスタンス一覧から先ほど作成したDBインスタンスのステータスを確認し、「利用可能」となっていれば作成は完了です。 13.セキュリティグループの設定 DBインスタンスをクリックし、「接続とセキュリティ」からセキュリティグループを選択。「インバウンドルールの設定を編集」を選択。 デフォルトの設定を削除し、下記のように設定します。 ソースには使用しているEC2インスタンスに設定したセキュリティグループIDを指定します。 ここまで設定したら「ルールを保存」してください。 14.エンドポイントを確認する 再度、RDSダッシュボードからDBインスタンスを選択し、エンドポイント欄の数値をどこかにコピーしておいてください。 EC2上のMySQLからDBインスタンスにデータを移行する ここからはターミナル操作でデータを移行する手順です。 15.EC2インスタンス上でバックアップを取る [ec2-user]$ mysqldump --databases railsdb -u root -p > /tmp/railsdb.sql 'railsdb' の箇所は現在MySQL上で使用している移行したいデータベース名に置き換えてください。 ここで要求されるパスワードはrootユーザーのパスワードを入力してください。 16.RDSのDBインスタンスにリストアする mysql -h [エンドポイント] -u admin -p < /tmp/railsdb.sql エンドポイントには先ほど確認したエンドポイントを入力し、'admin'のところには先ほど設定したRDSのユーザー名を入力してください。ここで求められるパスワードもRDS用のマスターパスワードを入力します。 Rails側の設定 ここからはRailsのdatabase.ymlの設定を変更します。 17.database.ymlを書き換える #変更前のdatabase.yml production: <<: *default database: <%= Rails.application.credentials.db[:database] %> username: <%= Rails.application.credentials.db[:username] %> password: <%= Rails.application.credentials.db[:password] %> socket: <%= Rails.application.credentials.db[:socket] %> 環境変数としてそれぞれ設定してありますがRDS用のuser名とpasswordに変更していきます。さらにhostの項目を追加します。 #localターミナル $ EDITOR=vim bin/rails credentials:edit credentials.yml db: database: 変更しない username: admin(RDS用のユーザー名) password: RDS用の設定したPW socket: 変更しない host: エンドポイント #変更後のdatabase.yml production: <<: *default database: <%= Rails.application.credentials.db[:database] %> username: <%= Rails.application.credentials.db[:username] %> password: <%= Rails.application.credentials.db[:password] %> socket: <%= Rails.application.credentials.db[:socket] %> host: <%= Rails.application.credentials.db[:host] %> これで移行自体は完了です。 RDSのtime-zone設定 上記の時点でRDSへ移行は完了しましたがRDSはデフォルトでtime-zoneがUTCで設定されています。 [ec2-user]$ mysql -h [エンドポイント] -P 3306 -u [RDSのユーザー名] -p mysql> show variables like '%time_zone%'; +------------------+------------+ | Variable_name | Value | +------------------+------------+ | system_time_zone | UTC | | time_zone | UTC | +------------------+------------+ mysql> SELECT NOW(); UTC時間が表示される system_time_zoneはいじれないようなのでtime_zoneの方を日本時間に設定する手順を紹介します。 1.AWSのRDSページにアクセスする 2.パラメータグループを選択する 3.パラメータグループの作成を選択 4.time_zoneの項目にAsia/Tokyoを選択し、保存する 5.ダッシュボードから変更を選択 6.DBパラメータグループに先ほど作成したグループを選択。 変更を保存する。 7.「アクション」から再起動を選択 再起動後、DBインスタンスの設定項目のパラメータグループが「同期中」となっていることを確認する。 [ec2-user]$ mysql -h [エンドポイント] -P 3306 -u [RDSのユーザー名] -p mysql> show variables like '%time_zone%'; +------------------+------------+ | Variable_name | Value | +------------------+------------+ | system_time_zone | UTC | | time_zone | Asia/Tokyo | +------------------+------------+ mysql> SELECT NOW(); 日本時間が表示される 上記のようになっていればOK.
- 投稿日:2022-01-16T22:23:59+09:00
Webの仕組みとHTTP通信について
ブラウザとサーバー ブラウザ:今表示されている画面 サーバー:様々なデータを保存している大きなPCのイメージ ブラウザではサーバーからファイルをダウンロードしてきて表示している。ファイルの中身にはHTML、CSS、JavaScriptなどの情報が格納されている。 (ブラウザ) ←←←←← (サーバー) ファイル (HTML、CSS、JS) HTTP通信について HTTPとはブラウザとサーバーのやりとりの方法 基本は、リクエストとレスポンス リクエスト →→→→→ (ブラウザ) (サーバー) ←←←←← レスポンス リクエストで欲しい情報をサーバーに投げて、サーバーがその情報を返す。 このリクエストとレスポンスのやりとりのルールを作るのがサーバーサイド言語。 HTTPの基本のメソッド4つ ①GET データの取得 Webページを表示するときに使う このページみたい →→→→→ (ブラウザ) (サーバー) ←←←←← はい、ページの情報 ②POST データの送信 フォームなどでデータを保存するときに使う データ保存して →→→→→ (ブラウザ) (サーバー) ←←←←← はい、保存したよ ③PUT データの更新 既存データを書き換えるときに使う データ変更して →→→→→ (ブラウザ) (サーバー) ←←←←← はい、変更したよ ④DELETE データの削除 既存データを削除するときに使う データ削除して →→→→→ (ブラウザ) (サーバー) ←←←←← はい、削除したよ IPアドレスとドメイン IPアドレス:サーバー内でデータに割り振られている住所のようなもの。数字で管理されている。 ドメイン:「〜〜.com」の部分。 IPアドレスだと分かりにくいため、DNSがIPアドレスとドメインを紐付けている。 Webサイトが表示されるまで(例:Qiita) qiita.comのIPアドレス教えて →→→→→ (ブラウザ) (DNS) ←←←←← はい、このIPアドレスだよ ↓ DNSから取得したIPにGETリクエスト →→→→→ (ブラウザ) (DNS) ←←←←← はい、HTML、CSS、JSはこれだよ 最後に こんな感じでWebサイトは表示されている。Web開発を学ぶ上で最低限の知識なのでアウトプットしました。
- 投稿日:2022-01-16T21:10:17+09:00
rails(Docker)+nginxでPermission denied @ rb_io_reopen - /myapp/log/puma.stdout.log (Errno::EACCES)が発生する
はじめに Rails環境をDockerを利用して環境構築していたのですが、Nginxをつないだところエラーが発生してほかの記事があまり参考にならなかったため自分なりにまとめていきます。 主にWSL2でDockerを使っている方がこのエラーの対象になるかと思います。 問題 DockerでRailsとNginxの環境を用意して、Railsコンテナのユーザーをapp(rootではない)にdocker-compose.ymlで変更した状態でコンテナを起動すると以下のエラーが発生します。 rails | * Listening on http://0.0.0.0:3000 rails | * Listening on unix:///myapp/tmp/sockets/puma.sock rails | Use Ctrl-C to stop rails | bundler: failed to load command: puma (/usr/local/bundle/bin/puma) rails | /usr/local/bundle/gems/puma-5.5.2/lib/puma/runner.rb:123:in `reopen': Permission denied @ rb_io_reopen - /myapp/log/puma.stdout.log (Errno::EACCES) rails | from /usr/local/bundle/gems/puma-5.5.2/lib/puma/runner.rb:123:in `redirect_io' rails | from /usr/local/bundle/gems/puma-5.5.2/lib/puma/single.rb:56:in `run' rails | from /usr/local/bundle/gems/puma-5.5.2/lib/puma/launcher.rb:181:in `run' rails | from /usr/local/bundle/gems/puma-5.5.2/lib/puma/cli.rb:80:in `run' rails | from /usr/local/bundle/gems/puma-5.5.2/bin/puma:10:in `<top (required)>' rails | from /usr/local/bundle/bin/puma:25:in `load' rails | from /usr/local/bundle/bin/puma:25:in `<top (required)>' rails | from /usr/local/lib/ruby/3.0.0/bundler/cli/exec.rb:58:in `load' rails | from /usr/local/lib/ruby/3.0.0/bundler/cli/exec.rb:58:in `kernel_load' rails | from /usr/local/lib/ruby/3.0.0/bundler/cli/exec.rb:23:in `run' rails | from /usr/local/lib/ruby/3.0.0/bundler/cli.rb:478:in `exec' rails | from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run' rails | from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command' rails | from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch' rails | from /usr/local/lib/ruby/3.0.0/bundler/cli.rb:31:in `dispatch' rails | from /usr/local/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start' rails | from /usr/local/lib/ruby/3.0.0/bundler/cli.rb:25:in `start' rails | from /usr/local/lib/ruby/gems/3.0.0/gems/bundler-2.2.32/libexec/bundle:49:in `block in <top (required)>' rails | from /usr/local/lib/ruby/3.0.0/bundler/friendly_errors.rb:103:in `with_friendly_errors' rails | from /usr/local/lib/ruby/gems/3.0.0/gems/bundler-2.2.32/libexec/bundle:37:in `<top (required)>' rails | from /usr/local/bin/bundle:23:in `load' rails | from /usr/local/bin/bundle:23:in `<main>' rails exited with code 1 Permission denied @ rb_io_reopen - /myapp/log/puma.stdout.log (Errno::EACCES)ということでpuma.stdout.logが権限的にみれないよとエラーを出しています。 調査 これはpumaのログがコンテナ内でroot権限で作成されることが原因です。 私の作成したdocker-compose.ymlは以下のようになっています。 docker-compose.yml version: "3.9" services: rails: build: . container_name: rails command: bundle exec puma -C config/puma.rb volumes: - .:/myapp - public-data:/myapp/public - tmp-data:/myapp/tmp - log-data:/myapp/log env_file: - .env depends_on: - db environment: WEBPACKER_DEV_SERVER_HOST: webpacker user: app webpacker: build: . volumes: - .:/myapp command: ./bin/webpack-dev-server environment: WEBPACKER_DEV_SERVER_HOST: 0.0.0.0 ports: - "3035:3035" db: image: mysql:8.0.27 container_name: db environment: TZ: Asia/Tokyo MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} ports: - "3306:3306" volumes: - db:/var/lib/mysql web: build: context: containers/nginx volumes: - public-data:/myapp/public - tmp-data:/myapp/tmp ports: - 80:80 depends_on: - rails volumes: db: driver: local bundle: driver: local public-data: tmp-data: log-data: ここで注目してほしいのはrailsコンテナのuser: appという箇所でユーザーを指定しているところです。これでコンテナ内でrails gなどでファイル生成してもappユーザー(uid=1000)で作成されるため、WSL2のユーザーと権限が同じなので編集ができるようになります。この設定をしないとrootユーザーでファイルが作成されるためWSL2ユーザーでは編集ができません。 しかし、appを指定したことでpumaログがroot権限で作成されているため編集できずエラーとなっていました。 ここで考えられるのは以下の方法です。 WSL2をrootでログインするように設定する コンテナ内の参照するフォルダ(と中身のファイル)の権限をappユーザーが開けるように変更する 今回はコンテナ内の/{workdir}/log(ここではmyapp/log)の権限を変更して対応していきます。 まずは実際に権限がrootになっているのか確認します。 いまのままではコンテナが落ちてしまうのでdocker-compose.ymlのrailsコンテナのコマンドを以下に変更します。 docker-compose.yml command: sh -c "while :; do sleep 10; done" && bundle exec puma -C config/puma.rb コンテナの中に入って以下のコマンドを打ちます。 # コンテナの起動 # $ docker-compose up # コンテナに入る # $ docker exec -it rails sh # 権限の確認 $ ls -la # rootになっている drwxr-xrwx 2 root root 4096 Jan 16 08:49 log この権限をappに変更できれば良さそうです。 こちらの記事によると、Volumesで設定するとrootになってしまうようです。 ここで2つの回避策を考えましたので紹介します。 回避策1 コンテナの中で権限を変更する コンテナの中で直接権限を変更します。 注意としてはrootでコンテナに入る必要があるので、docker-compose.ymlのuserをコメントにします。 rootであれば権限問題が起きないのでコンテナは立ち上がるかと思います。 # 別ターミナルを開く # コンテナに入る $ docker exec -it rails sh # 権限を変更する $ chown app:app -R /myapp/log chownでエラーが出る場合はrootユーザーになっていないのが原因かと思いますのでid -u -nコマンドで確認してください。 一度変更すれば以降は変更された状態になります。 しかし、ボリュームを削除したり、gitからダウンロードした場合にrootでアクセスしてdocker-compose.ymlを編集してuser:appに変更するという手間も発生するので微妙かなとは思いました。 回避策2 Nginxで権限変更のシェルを実行する 1の欠点を補うためにNginxコンテナを起動するときにシェルを走らせることで回避することにしました。 まず準備としてdocker-compose.ymlのNginxのボリュームを変更します。 docker-compose.yml web: build: context: containers/nginx volumes: - public-data:/myapp/public - tmp-data:/myapp/tmp - log-data:/myapp/log ports: - 80:80 depends_on: - rails myapp/logをvolumesに設定しました。 そして、Dockerfileでentrypoint.shを設定できるようにします。 FROM nginx:alpine # インクルード用のディレクトリ内を削除 RUN rm -f /etc/nginx/conf.d/* RUN adduser -D app -u ${UID:-1000} COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] # Nginxの設定ファイルをコンテナにコピー ADD nginx.conf /etc/nginx/conf.d/myapp.conf # ビルド完了後にNginxを起動 CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf 次にnginxのDockerfileと同じディレクトリにentrypoint.shを作成します。 entrypoint.sh #!/bin/sh set -e chown app:app -R /myapp/log exec "$@" ここまでの設定を行うことで、Nginxコンテナが立ち上がったときにentrypoint.shが実行され、volumesで設定されているmyapp/logの権限をappに変更します。また同じユーザーをDockerfileで作成することも忘れないようにしてください。 nginx自体はrootユーザーで起動するため権限変更chownが利用でき変更が可能です。 railsコンテナで同じように権限を変更しようとなるとrootでアクセスする必要があるため今回はnginxで権限を変更するように設定しました。 回避策3 マウントに権限を付与する マウントで権限を与える方法が分かりました。 tmpfsマウントを使うと権限を与えることが可能になります。 今回はコンテナ間で共有しないのでこれで良さそうです。 docker-compose.yml tmpfs: - /myapp/log:exec,mode=777,uid=1000,gid=1000 解決策 解決策はいたってシンプルでDockerfileでユーザーを追加して、ディレクトリコピーの時にchownすればOKでした。 FROM ruby:alpine3.13 ARG UID RUN adduser -D app -u ${UID:-1000} && \ # user追加 apk update \ && apk add --no-cache gcc make libc-dev g++ mariadb-dev tzdata nodejs~=14 yarn WORKDIR /myapp COPY Gemfile . COPY Gemfile.lock . RUN bundle install COPY --chown=app:app . /myapp # 権限を付与してコピー RUN chown app:app -R /myapp/node_modules/.bin COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] # EXPOSE 3000 # CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] USER app # ユーザー変更 RUN mkdir -p tmp/sockets # フォルダ作成はユーザー設定してから RUN mkdir -p tmp/pids おわりに 権限問題が複雑に絡まっており解決するまで時間がかかりました。 調べてもよくわからず自分なりに工夫をして解決することができました。 参考 dockerでvolumeをマウントしたときのファイルのowner問題
- 投稿日:2022-01-16T19:46:54+09:00
Rails × Vue で vue Router を使用する
rails アプリの中でVueを使用する場合にVue Routerを使ったページの切り替えができるまでの過程を書いていきます。 Vue Routerとは Vue Routerとは、Vue.jsを使ったアプリケーションでルーティング制御をするためのプラグインのことです。これを利用することでリクエストされたURLごとに表示するページを切り替えることができるようになります。 Vue Routerを使いページの切り替えができるようになるまで Vue Routerでページの切り替えをするには Vue Routerのインストール Vue Routerの設定 application.jsファイルでVue Router の設定をimportする 表示したいページ(コンポーネントの用意) rails側のルーティングの設定 が必要となるので順番に進めていきます。 Vue routerのインストール まずは Vue Routerをインストールしていきます。インストールの仕方は様々だと思いますが自分の環境では以下のコマンドでインストールを行いました。 yarn add vue-router これだけでインストールは完了です。次のステップに進みます。 Vue Routeの設定 インストールしたVue Routerで実際のルーティングを制御するファイルを作成していきます。まだ作成していないコンポーネントをインポートしますがこちらは後で作成します。 app/javascript/router.js というファイルを作成し、中身を import Vue from 'vue' import Router from 'vue-router' import Top from "../pages/top" import Test from '../pages/test' Vue.use(Router) export default new Router({ name: "Router", mode: 'history', routes: [ { path: '/', name: "top", component: Top, }, { path: '/test', name: "test", component: Test, }, ] }) とします。 application.jsファイルに Vue Router の設定をimportする 先ほど設定したURLごとのコンポーネントの切り替えを反映するためのコードを app/javascript/application.js に追加します。 import Vue from 'vue' import App from '../app.vue' import Router from'../router.js' // 追加 router.jsファイルのimport import 'bootstrap/dist/css/bootstrap.css' Vue.config.productionTip = false document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ render: h => h(App), router: Router // 追加 router.jsに書いたルーティングごとに切り替えを使用できるようになる }).$mount() document.body.appendChild(app.$el) }) 表示したいページの準備 URLごとに表示するページ(コンポーネント)を作成していきます。 トップページの作成 ますはトップページを作成していきます。 app/javascript/pages/top.vue というファイルを作成し、中身を <template> <p>topページです</p> </template> <script> export default { name: "Top", } とします。 切り替え先ページの作成 app/javascript/pages/test.vue というファイルを作成し、中身を <template> <p>testページです</p> </template> <script> export default { name: "Test", } とします。 最後にHTMLファイルに実際に読み込まれる app/javascript/app.vue ファイルの変更を行います。(上で用意した二つのコンポーネントはこのファイルの中に埋め込まれる) <template> <div> <router-view /> // この部分にURLごとのファイルが埋め込まれる </div> </template> <script> export default { name: "App" } </script> Vue Routerを使用すると各コンポーネントは<div>タグの中に記述されている<router-view />の中に埋め込まれるようになるなります。これでページの準備は完了です。 rails側のルーティングを設定する rails側ではどんなURLにリクエストが来たとしても表示するページはひとつとなります。その中で埋め込むページを切り替えるのはVue側の役目となります。なのでrouter.rbの中身を以下とします。 Rails.application.routes.draw do root to: 'home#index' get '*path', to: 'home#index' # どのURLにリクエストが来たとしてもhome#indexアクションを使用する end これで全ての設定が完了です。 サーバーを起動し、http://localhost:3000、http://localhost:3000/test にアクセスすると表示が切り替わるようになります。
- 投稿日:2022-01-16T19:06:53+09:00
Rails 7でTailwind CSSを使ってみた
Rails 7の新機能を触ってみたいと思い、今回はその中でもTailwind CSS for Railsというgemを使ってNode.jsやWebpack無しでモダンなフロントエンド開発環境をお試しで作ってみたいと思います。 脱モジュールバンドラー Rails 7では、cssbundling-railsというGemをインストールすることで、 Tailwind CSS、 Bootstrap、 Bulma、PostCSS、 Dart Sassを使用してCSSを結合・最適化し、Railsのアセットパイプラインで配信ができるようになったそうです。Webpackerなどのバンドラーを使わなくとも良くなったということですね。 その中でも今回はTailwind CSSを使ってみました。 ちなみにバンドラーを使わないメリット等含め、Rails 7におけるフロントエンド周りの刷新についてDHH氏のブログ記事をまとめた以下記事が参考になりました。 Tailwind CSS is 何? 本筋から逸れてしまうため、簡単に自分なりにまとめると あらゆるスタイリングに対応したクラスをまとめたライブラリ クラス名にはh-12 w-12 p-6 max-w-sm mx-autoのように適用したいスタイルの名前を指定をする BEMのようなレイアウト上意味のある命名をするやり方とは真逆のアプローチ メリットは命名に悩む時間が減る、CSSファイルが無闇に増えない、安全な変更が可能。レスポンシブデザインにも対応 保守性はコンポーネント化することでカバーする。つまり、ReactやVue.jsなどのライブラリ使用が前提 といった内容のようです。 Tailwind CSS、使ってみた インストール 新規アプリケーション作成の場合、rails newにオプションを付けるだけです。 $ rails new myapp --css tailwind 既存アプリケーションの場合はGemをインストールした上でTailwind CSSを入れましょう。 Gemfile gem 'cssbundling-rails' $ bin/bundle install $ bin/rails css:install:tailwind サンプルTodoモデル作成 scaffoldでTodoモデルを作成してみます。 $ rails g scaffold todo name:string limit:date # Tailwind CSSのビルドが必要 $ rails tailwindcss:build rails sでサービスを起動している場合、手動でCSSをビルドする必要があります。 上記ビルドコマンドを実行することでapp/assets/builds/tailwind.cssがビルドされます。 CSSを修正する度に手動で再ビルドが必要なのはイケてないのですが、rails sではなくbin/devでサービスを起動することにより、Tailwindのスタイル修正の都度、自動でビルドが走るようになるみたいです。 ・・・が、私のDocker環境でrails sの代わりにbin/devで起動すると、起動と同時にサービスが落ちてしまい上手く動作しませんでした。エラーログも出ないため、一旦手動でビルドします。これが解消できないと実用は厳しいかな。。 動作確認 これは驚いたんですが、scaffoldで自動作成されたviewの各DOMのクラス名にTailwind CSSを適用するクラス名が自動で設定されています。すごい! これで無事Tailswind CSSが適用されていることを確認できました。 index.html.erb <div class="flex justify-between items-center"> <h1 class="font-bold text-4xl">Todos</h1> <%= link_to 'New todo', new_todo_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %> </div> <div id="todos" class="min-w-full"> <%= render @todos %> </div> 使ってみた所感 導入だけは何とかできたがTailwind CSSを使いこなすには学習コストが多くかかりそう 当たり前だがサンプルアプリ程度の規模ではバンドラーを使わないメリットは享受しにくい リリース間もない機能を使ってみたのが初めてで、情報を集めることの難しさを知った 本当はHotwireやTurboを使って簡単なSPAを作るところまでやりたかった… まだ使用例も少ない中、自分の力では導入して動作を試すところが精一杯でした。 次はもう少し突っ込んだところまでやれたらいいなと思います。 参考記事
- 投稿日:2022-01-16T14:57:59+09:00
rails5.2.6で複数モデルのDM機能(ActionCable)を実装
はじめに DM機能でActionCableを使ってみた。 ActionCableを使えばページをリロードしなくてもメッセージがリアルタイムに表示されるのでかっこいい。 お世話になった記事 ほとんど知識がなかったので下記のサイトを参考にさせていただきながら作成しました。 リアルタイムチャットは誰でもつくれる~Action CableでDM機能を作ろう~ [Rails5.2 ActionCable]シンプルなチャットアプリを作ってみた!! Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より) 開発環境 ruby 2.6.3 Rails 5.2.6 前提条件 DM機能を一から作っているので前半不要な方は読み飛ばしてください。 今回はユーザーIDを用いて「誰とチャットしているのか、誰が発言しているのか」を明確にするため、以下のようなER図で開発します。 *User・・・deviseを使用 *Room・・・トークルーム *Message・・・1対1でのやり取り *RoomUser・・・中間テーブル RoomとUserをつなげるイメージ。Userが2人入る。Room1つに対してRoomUserは2つできる感じ。 作成開始 rails new rails new ActionCable jqueryの導入 Gemfile gem 'jquery-rails' $ bundle install application.jsに下記2行を追加 app/assets/javascripts/application.js //= require rails-ujs //= require activestorage //= require turbolinks //= require jquery *追加* //= require jquery_ujs *追加* //= require_tree . ユーザー認証機能の導入 Gemfile gem 'devise' $ bundle install deviseの初期設定 $ rails g devise:install モデルの作成+カラム(name)の追加 $ rails g devise User name:string $ rails db:migrate nameカラムでログインできるようにする app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) end end viewページの生成 今のままだと新規登録時にemailとpasswordを入力する項目しかないため。 ※http://localhost:3000/ $ rails g devise:views 新規登録画面の表示内容を編集 app/views/devise/registrations/new.html.erb : <!-- 追加 --> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name, autofocus: true %> </div> <!-- ここまで --> <div class="field"> <%= f.label :email %> : ログアウトボタンの表示 新規登録orログイン後はログアウトボタンがないと一生ログインしたままになるので、ログアウトボタンを表示させます。 app/views/layouts/application.html.erb : <body> <% if user_signed_in? %> <li> <%= link_to "ログアウト", destroy_user_session_path, method: :delete %> </li> <% else %> <li> <%= link_to "新規登録", new_user_registration_path %> </li> <li> <%= link_to "ログイン", new_user_session_path %> </li> <% end %> <%= yield %> </body> : これで、一度新規登録してみます。 Yay! You’re on Rails! が表示されればOKです。 必要モデルの作成 先ほどのER図を確認しながら、Roomモデル、RoomUserモデル、Messageモデルを作成していきます。必要なカラムも同時に追加。 (外部キーreference型で追加した方がいいのかと思いつつintegerで追加してしまった、、、 ?参考サイト Railsの外部キー制約とreference型について) $ rails g model Room name:string $ rails g model RoomUser user_id:integer room_id:integer $ rails g model Message user_id:integer room_id:integer text:string $ rails db:migrate モデルと必要カラムの作成が完了しました。 アソシエーションの設定 app/models/user.rb : has_many :messages, dependent: :destroy has_many :room_users, dependent: :destroy : app/models/room.rb class Room < ApplicationRecord has_many :messages, dependent: :destroy has_many :room_users, dependent: :destroy end app/models/room_user.rb class RoomUser < ApplicationRecord belongs_to :user belongs_to :room end app/models/message.rb class Message < ApplicationRecord belongs_to :user belongs_to :room end コントローラの作成 usersコントローラ・・・index,show roomsコントローラ・・・show $ rails g controller users index show $ rails g controller rooms show ルーティングの設定 config/routes.rb root to: 'users#index' devise_for :users resources :users, only: [:show] resources :rooms, only: [:show, :create] ユーザー関連のページを表示させる ユーザー一覧ページ ユーザーの一覧をまとめて表示させるページ。 ここから、ユーザー詳細ページに飛ぶ。 app/controllers/users_controller.rb def index @users = User.all end def show end app/views/users/index.html.erb <h1>ユーザー一覧ページ</h1> <% @users.each do |user| %> <%= link_to user.name, user_path(user.id) %> <% end %> ユーザー詳細画面を表示 ユーザー詳細画面から、トークを始められるようにする app/controllers/users_controller.rb : def index @users = User.all end def show @user = User.find(params[:id]) #RoomUserモデルから自分のものを検索 @current_user_room_user = RoomUser.where(user_id: current_user.id) #RoomUserモデルから今開いているページのユーザーのものを検索 @user_room_user = RoomUser.where(user_id: @user.id) #もし今開いてるユーザーページが自分のページじゃなかったら unless @user.id == current_user.id #自分が関係あるRoomUserをeachで引っ張り出してくる @current_user_room_user.each do |cu| #かつ、今開いているページのユーザーに関係あるRoomUserをeachで引っ張り出してくる @user_room_user.each do |u| #もし一致するものがあったら if cu.room_id == u.room_id then #すでに部屋があるということ @is_room = true #そのroomのid @room_id = cu.room_id end end end #一致する部屋がなかったら unless @is_room #新しく作る @room = Room.new @room_user = RoomUser.new end end end : app/views/users/show.html.erb <h1>ユーザー詳細ページ</h1> <h2>ユーザー名</h2> <%= @user.name %> <h2>トーク関連</h2> <% if @is_room == true %> <%= link_to "トークを始める", room_path(@room_id) %> <% else %> <!-- roomsテーブルに情報を送信 --> <%= form_with model: @room, local: true do |f|%> <!-- room_usersテーブルに情報を送信 --> <%= hidden_field_tag 'room_user[user_id]', @user.id %> <%= f.submit "チャットを始める", class: "send" %> <% end %> <% end %> すでにトークルームがある時=> @is_room = true トークルームへのリンクへ飛ぶ 新しくトークを始める時=> form_with でルームを新しく作る。RoomUserも一緒に作る。 トークルームを作る トークルームとRoomUserを作る app/controllers/rooms_controller.rb class RoomsController < ApplicationController def create @room = Room.create # RoomUserを2つ作る # 自分のRoomUserを作る @join_current_user = RoomUser.create(user_id: current_user.id, room_id: @room.id) # 相手のRoomUserを作る @join_user = RoomUser.create(join_room_params) redirect_to room_path(@room) end def show @room = Room.find(params[:id]) end private def join_room_params params.require(:room_user).permit(:user_id, :room_id).merge(room_id: @room.id) end end 1つのRoomに2つのRoomUserが作られた状態ができた。2人のuserを呼び出す。 トークルームでメッセージを表示させる app/controllers/rooms_controller.rb : def show @room = Room.find(params[:id]) <!-- 追加 --> #roomに関するRoomUserが存在していれば if RoomUser.where(user_id: current_user.id, room_id: @room.id).present? @messages = @room.messages @message = Message.new @room_users = @room.room_users else redirect_back(fallback_location: root_path) end <!-- ここまで --> end : ビューページを作る app/views/rooms/show.html.erb <h1>ルーム詳細ページ</h1> <h2>参加者</h2> <!-- roomに入っているroom_userは2人いるのでeachで引っ張る --> <% @room_users.each do |e| %> <%= link_to e.user.name, user_path(e.user_id) %> <% end %> <h2>トーク</h2> <!-- 入力フォーム --> <!-- form_withだと余計なものが生成されるので<form>タグで実装 --> <form> <input type="text" data-behavior="room_speak"> </form> <% if @messages.present? %> <% @messages.each do |m| %> <div> <%= m.text %> <%= m.created_at.strftime("%Y-%m-%d %H:%M") %> </div> <% end %> <% end %> 部分テンプレで切り出し 先ほどのroom/show.html.erbから メッセージを表示させる部分だけ部分テンプレートとして切り出します。 app/views/rooms/show.html.erb : <% if @messages.present? %> <% @messages.each do |m| %> <!-- 削除 --> <div> <%= m.text %> <%= m.created_at.strftime("%Y-%m-%d %H:%M") %> </div> <!-- ここまで --> <% end %> <% end %> app/views/rooms/show.html.erb <% if @messages.present? %> <% @messages.each do |m| %> <!-- 追加 --> <%= render "messages/message" %> <!-- ここまで --> <% end %> <% end %> : viewsフォルダ直下に、messagesフォルダと_message.html.erbを作成します。 app/views/messages/_message.html.erb : <div> <%= m.text %> <%= m.created_at.strftime("%Y-%m-%d %H:%M") %> </div> : ここまででUserモデル、Roomモデル、RoomUserモデルの作成、定義と下準備が完了したので やっと、ActionCable導入へと移ります! チャネルを作成する $ rails g channel room speak speakはメソッド名です。 クライアントサイドの処理をするファイル app/assets/javascripts/channels/room.coffee app/assets/javascripts/cable.js サーバーサイドの処理をするファイル app/channels/room_channel.rb 以上3ファイルをいじりながら実装していきます。 設定 クライアント-サーバー間のやり取りを設定 app/channels/room_channel.rb class RoomChannel < ApplicationCable::Channel def subscribed #クライアントが受信するストリームを設定 stream_from "room_channel" #追加 end : app/assets/javascripts/channels/room.coffee : speak: (message) -> #performメソッドでブラウザから入力されたデータをサーバーサイドへ送信 @perform 'speak', message: message # 'keypress' キーが押された時発火する $(document).on 'keypress', '[data-behavior~=room_speak]', (event) -> # key13= returnキー if event.keyCode is 13 # event.target.valueは入力されたデータ App.room.speak event.target.value event.target.value = '' event.preventDefault() : returnキーを押したときに発火されサーバーサイドへデータが送信されるようにする。 受け取ったデータをアラートで表示してみる app/assets/javascripts/channels/room.coffee : received: (data) -> alert data['message'] # Called when there's incoming data on the websocket for this channel : フォームに入力したデータが、アラートで表示されました。 今度はブラウザ上に表示してみる app/assets/javascripts/channels/room.coffee : received: (data) -> alert data['message'] #削除 $('#messages_all').append data['message'] #追加 : ブラウザに表示されました。データベースに保存していないので、ページ更新すると消えてなくなります。 次にMessageモデルにデータを保存する処理を作っていきたいと思います。 データ保存&非同期処理 Active Jobというものを導入して、非同期処理できるようにする。 room_channel.rbのspeakアクションを書き換える app/channels/room_channel.rb def speak(data) # current_userは使えないので別途定義するか、rooms/showページからidを渡す # rooms/show.html.erbのdiv idからmessage,user_id,room_idを受け取る Message.create! text: data['message'], user_id: data['user_id'], room_id: data['room_id'] end Messageモデルにデータを保存するとき、messageの内容だけでなく、user_idとroom_idを受け取り生成する必要があります。 user_idとroom_idが渡るように記述を変更 app/views/messages/_message.html.erb <% if @messages.present? %> <% @messages.each do |m| %> <div> <%= m.text %> <%= m.user.name %> <!-- 追加 --> <%= m.created_at.strftime("%Y-%m-%d %H:%M") %> </div> <% end %> <% end %> app/views/rooms/show.html.erb : <h2>トーク</h2> <!-- 入力フォーム --> <div id="message" data-room_id="<%= params[:id] %>" data-user_id="<%= current_user.id %>"> <!-- form_withだと余計なものが生成されるので<form>タグで実装 --> <form> <input type="text" data-behavior="room_speak"> </form> </div> : app/assets/javascripts/channels/room.coffee : speak: (message) -> #performメソッドでブラウザから入力されたデータをサーバーサイドへ送信 @perform 'speak', { message: message, room_id: $('#message').data('room_id'), user_id: $('#message').data('user_id') } : ブロードキャスト処理の実行 rails g job MessageBroadcast 生成されたファイルを編集していきます。 app/models/message.rb #無記入投稿とエンター長押し連続投稿を防ぐ validates :text, presence: true #MessageBroadcastJobを走らせるタイミングはMessageがcreateされた後 after_create_commit {MessageBroadcastJob.perform_later self} Messageがクリエイトされた後、ブロードキャスト処理が走るように一文を記述します。 app/jobs/message_broadcast_job.rb class MessageBroadcastJob < ApplicationJob queue_as :default def perform(message) ActionCable.server.broadcast "room_channel", message: render_message(message) end private def render_message(message) ApplicationController.renderer.render partial: 'messages/message', locals: { message: message } end end 完成です 課題 current_userが渡らない問題 deviseを導入すると使えるcurrent_user、そのままActionCableに渡せない問題。 解決策は二つありそうです。 ①viewページからcurrent_userのIDを渡す 今回はこっちで実装しました ?参考にさせていただいたサイト Action Cableへのidの受け渡し ②ActionCableのconnection.rbで別途current_userを定義する ?参考にさせていただいたサイト Rails公式ドキュメント 【ActionCable】チャンネル接続/購読時にユーザ認証を行う ActionCableにおいてのcurrent_userについて読み解く|Rails デプロイするまで完成したかわからない 本当はブラウザを二つ開いてそれぞれ別のユーザーでログインして動作チェックをしたいんだけど、本番環境でないとできないので、ここで終わりです。 間違っている内容がありましたら、コメントでぜひ教えてください。
- 投稿日:2022-01-16T14:54:40+09:00
【Rails】Rails7.0でyarn build yarn build:cssが起動しない
前提 Rails 7.0.1 Ruby 2.7.2 docker 20.10.11 発生したエラー rails new -j esbuild で作成した後にサーバーを起動させる際にyarn buildとyarn build:cssがエラーが出てバンドルされない状態になってしまう。 console ~/rails(master) $ bin/dev 20:59:27 web.1 | started with pid 94024 20:59:27 js.1 | started with pid 94025 20:59:27 css.1 | started with pid 94026 20:59:27 js.1 | yarn run v1.22.15 20:59:27 css.1 | yarn run v1.22.15 20:59:27 js.1 | error Command "build" not found. 20:59:27 js.1 | info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. 20:59:27 css.1 | error Command "build:css" not found. 20:59:27 css.1 | info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. 20:59:28 js.1 | exited with code 1 20:59:28 system | sending SIGTERM to all processes 20:59:28 css.1 | exited with code 1 20:59:28 web.1 | terminated by SIGTERM 本文 bin/devコマンドでサーバーを起動させるとforemanがインストールされ、プロジェクトディレクトリ直下にあるProcfile.devが実行されます bin/dev #!/usr/bin/env bash if ! command -v foreman &>/dev/null; then echo "Installing foreman..." gem install foreman fi foreman start -f Procfile.dev サーバーの起動と別プロセスでjsとcssのバンドルが行われ、yarnのコマンド使用して実行されています。 Prodfile.dev web: bin/rails s -b '0.0.0.0' -p 3000 js: yarn build --watch css: yarn build:css --watch エラー文ではerror Command "build" not found と出ているのでpackage.jsonでコマンドが設定されていないことでエラーが出ています。 コマンドを手動で追加します。 今回はcssをBootstrapを利用してますので、他のcssを利用している場合はコマンドを各自書き換えてください package.json { "name": "app", "private": "true", "dependencies": { "@hotwired/stimulus": "^3.0.1", "@hotwired/turbo-rails": "^7.1.0", "@popperjs/core": "^2.11.2", "autoprefixer": "^10.4.2", "bootstrap": "^5.1.3", "esbuild": "^0.14.11", "postcss": "^8.4.5", "sass": "^1.48.0", }, "scripts": { "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules", "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds" //上記を追加する } } これによってサーバー起動時にbin/devを利用することでjsとcssのバンドルをやってくれるようになります。 まとめ rails6の時のようにちょっとだけファイルの変更があったとしても、ほんの数秒でバンドルをしてくれるのでDXがめちゃくちゃいいです。 色々と変更点が多いのでいきなりバージョンを上げるのはもう少し待ったほうが良さそうです。
- 投稿日:2022-01-16T14:52:19+09:00
Railsコマンド
前提 参考書籍 小餅良介著『独習Ruby on Rails』SE出版 実行環境 Ruby on Rails ver5.2.2 コマンド $ rails new アプリケーション名 [オプション] アプリケーションを作成 -O : Active Record の生成をスキップ -d : 使用するデータベースを指定 -T : 表人で用意されているテスト関連ツールのフレームセットの生成をスキップ -B : bundle install をスキップ --skip-keeps : 標準フレームのディレクトリに空のディレクトリ(concernsやimagesなど)の生成をスキップ --api : ActionController::API を継承したコントローラーを生成 $ rails generator(g) ジェネレータの種類 引数 新しいモデルやコントローラのひな型を生成 $ rails g controller コントローラ名 [アクション名[...]] コントローラを生成 アクション名を指定すると対応するルートをroutes.rbに追加し、ビューテンプレートのスケルトンをviewディレクトリに生成 $ rails destroy(d) ジェネレータの種類 引数 [オプション] generatorコマンドの取り消し $ rails server(s) Railsサーバ(Puma)を立ち上げてアプリケーションを実行 $ rails console(c) Railsサーバを立ち上げずに対話的にアプリケーションの実相を確認 --sandbox : コマンド終了時にデータベースに対する更新処理をロールバック $ rails dbconsole(db) データベースのコマンドラインツールを起動 ※--sandboxは使えない $ rails runner(r) Webアプリケーションの外からバッチ処理を実行
- 投稿日:2022-01-16T13:13:26+09:00
Rails ディレクトリ構成・各種ディレクトリとファイルの役割
前置き 参考書籍 小餅良介著『独習Ruby on Rails』SE出版 環境 Ruby on Rails ver.5.2.2 ディレクトリ構成(一階層) アプリケーションディレクトリ ├ .git ├ app ├ bin ├ config ├ db ├ lib ├ log ├ public ├ storage ├ test ├ tmp ├ vendor ├ .gifitignore ├ .ruby-version ├ config.ru ├ Gemfile ├ Gemfike.lock ├ package.json ├ Rackfile └ README.md 各ディレクトリの役割 .git Gitが利用するディレクトリ app アプリケーションに関する情報を管理するディレクトリ bin スクリプトファイルを管理するディレクトリ config 実行環境に関する設定が入ったディレクトリ db データベースの設定情報を管理するディレクトリ lib ライブラリを管理するディレクトリ log ログファイルを管理するディレクトリ public 静的ファイルを管理するディレクトリ storage Rails 5.2で追加されたActive Storageのディレクトリ(変更可) test テストコード・データを管理するディレクトリ tmp 一時的な情報(キャッシュ、プロセスID、セッションなど)を管理するディレクトリ vendor サイドパーティ製のリソースを管理するディレクトリ 各ファイルの役割 .gitignore gitの管理対象から外すファイルを管理するファイル .ruby-version Rubyのバージョンを管理するファイル config.ru RackがRailsサーバを起動するために使用する設定ファイル Gemfile Gemパッケージの設定ファイル Gemfile.lock Gemパッケージの依存関係を管理するファイル package.json npmを使用する際に使うファイル Rackfile Rakeタスクコマンドの実行を管理するファイル README.md アプリケーションの説明書 ディレクトリ構成(appディレクトリ) ─ app ├assets │ ├images │ ├javascript │ └stylesheets ├controllers │ └concerns ├helpers ├ajobs ├mailers ├models │ └concerns └views ├layouts └(各コントローラ) 各ディレクトリの役割 assets アセット(CSS、JavaScript、画像ファイルなど)ファイルを管理するディレクトリ images 画像を配置するためのディレクトリ javascripts JavaScriptsファイルを配置するためのディレクトリ stylesheets CSSファイルを配置するためのディレクトリ cntrollers コントローラクラスを管理するディレクトリ concerns コントローラ共通のコードを管理するディレクトリ helpers ヘルパーモジュールを管理するディレクトリ jobs ジョブクラスを管理するディレクトリ mailers メール制御クラスを管理するディレクトリ models モデルクラスw管理するディレクトリ concerns モデル共通コードを管理するディレクトリ views ビューテンプレートを管理するディレクトリ layouts 共通レイアウトを管理するディレクトリ 各コントローラ名のディレクトリ 各コントローラに対応するビューテンプレートを管理するディレクトリ 準備中・・・
- 投稿日:2022-01-16T12:40:48+09:00
1回のフォーム送信で複数のモデルを保存する方法
今回は一つのリクエストで、2つのモデルに対して保存処理を行う方法についてまとめます。例えば購入記録を保持するテーブルと、それに紐づく住所を保持するテーブルがあるとします。この時、どうすれば一回のリクエスで二つのテーブルに対しえて保存処理を行えるのでしょうか。その解決方法としてFormフォームオブジェクトパターンを新しく学んだので、備忘録としてまとめます。 Formフォームオブジェクトパターン Formフォームオブジェクトパターンとは、一つのフォーム送信で複数のモデルを操作したい時に使います。一つのフォーム送信で複数のモデルを操作する時には例えば以下のような問題が発生します。 ①一度で複数のモデルのバリデーションを通過させないといけない ②バリデーションで弾かれた場合、複数のモデルのエラーメッセージを表示させないといけない。 これらを実装しようとすると複雑なコードを書かなくてはなりません。 そういった問題を解決するためにFormフォームオブジェクトパターンを使います。 使い方としては ①modelsディレクトリ直下に自作ファイルを作成し、複数のモデルに関する処理をまとめて記述する ②コントローラーに自作したクラスのインスタンス変数を渡す 順番に解説していきます。 『今回はどこかにお金を送金するような仕組みを考えます。 モデルとしては次の二つです。 ①moneysテーブル(:money、:user_id) ②addressテーブル(:city, :house_number,:money_id) この2つのモデルを1回のリクエストで保存するという処理です』 新たにmodelsディレクトリ直下にファイルを作成し、クラスを定義する まずはmdoels直下に自作ファイルを作ります。moneysテーブルとaddressテーブルをまとめて、「money_address.rb」とします。 そして次に、この自作したクラスの中に、2つのテーブルのカラムをまとめて記述します。 money_address.rb class OrderShipping include ActiveModel::Model attr_accessor :user_id, :money, :city, :house_number end ここでの注意点は、以下の二つです ①attr_accessor→ゲーターとセッターを定義してくれるメソッド。 ②include ActiveModel::Model→ActiveModel::Modelをincludeすることで、form-withの引数にできたり、バリデーションの設定ができたりします。 自作クラスにバリデーションを設定する 次に自作したmoney_address.rbにmoneyテーブルとaddressテーブルのバリデーションを全て設定します。 money_address.rb class MoneyAddress include ActiveModel::Model attr_accessor :user_id, :money, :city, :house_number with_options presence: true do validates :user_id validates :money validates :city validates :house_number end 自作したクラスにバリデーションが設定できるのは、ActiveModel::Modelをincludeしているからです。 自作クラスに保存する処理を設定する 次に自作したorder_shippng.rbにmoneyテーブルとaddressテーブル、両方を保存する処理を加えます。 money_address.rb class MoneyAddress include ActiveModel::Model attr_accessor :user_id, :money, :city, :house_number with_options presence: true do validates :user_id validates :money validates :city validates :house_number def save money = Money.create(user_id: user_id, money: money) Address.create(city: city, house_number: house_number, money_id: money_id) end end 以上で自作クラスの設定は完了です。 コントローラーで自作クラスを呼び出す コントローラーで、以下のように自作クラスを呼び出します。 money_controller.rb class MoneyController < ApplicationController def index end def new @money_address = MoneyAddress.new end def create @money_address = MoneyAddress.new(money_params) if @money_address.valid? @money_address.save redirect_to root_path else render :new end end private def money_params params.require(:money_address).permit(city: city, house_number: house_number, money: money).merge(user_id: current_user.id) end end @money_address.valid?で自作クラスで定義したバリデーションを全て実行し、 @money_address.saveで自作クラスで定義した二つのモデルを保存しています。 Formフォームオブジェクトパターンの実装が完です。 これによりviewのform-withに@money_addressを渡すことができるので、 ①一度で複数のモデルのバリデーションを通過させないといけない ②バリデーションで弾かれた場合、複数のモデルのエラーメッセージを表示させないといけない。 という問題を解決できます。 まとめ 今回は一つのフォーム送信で複数のモデルを操作する時に使う、Formフォームオブジェクトパターンについてまとめました。初学者にとっては迷いやすい部分だと思うので、今後も学んでいきたいと思います。
- 投稿日:2022-01-16T12:21:02+09:00
[Elixir]RubyやRailsのメソッドは便利なものが多いので、それらをElixirでも使えるようにしたライブラリをリリースしました。
多言語を習得する際の困り事 「Rubyだと配列からnullを排除する時 compactで出来たけど、Elixirだとどう書くんだろう。。 あれ、Enum.select って無いのか Enum.filterを使えばいいのね!」 Elixirを学習中に、上記のような ちょっと調べればわかるけれども、ちょっと調べないといけないような事が、ちょくちょく発生しました。 RubyやRailsのEnumerable系メソッドをElixirでも使えるようにしました! RubyやRailsはその歴史の長さや、コミュニティの厚さにより、便利なmethodや直感的なaliasが多数定義されてます。 それらの資産を有効活用できないかと思い、REnumという、ElixirのmoduleをRuby/Rails風に拡張したライブラリをリリースしました。 なぜ、Enum(Enumerable)等に焦点を当てたかというと、Elixirの得意とするデータ変換にはEnum(Enumerable)等の拡張がもっとも有意に働くと思ったからです。 ElixirはRubyの文法の影響を大きく受けた言語でもあるので、Ruby => Elixirを学ぶ方はとても多いと感じております。そのような方の助けになりましたら幸いです。僕もそのうちの一人です。 機能紹介 RubyのEnumerableから移植してきたおすすめな関数達 compact(enumerable) 与えられたenumerableからnil以外のすべての要素を排除。 iex> [1, nil, 2, 3] iex> |> REnum.compact() [1, 2, 3] each_slice(enumerable, amount, func) amount毎に与えられたenumerableを分割し、各ブロックにfuncを適応してenumerableを返却。 iex> ["a", "b", "c", "d", "e"] iex> |> REnum.each_slice(2, &IO.inspect(&1)) # ["a", "b"] # ["c", "d"] # ["e"] ["a", "b", "c", "d", "e"] tally(enumerable) 等しい要素をkey, それらのカウントをvalueに持つマップを返却。 iex> ~w(a c d b c a) iex> |> REnum.tally() %{"a" => 2, "c" => 2, "d" => 1, "b" => 1} grep(enumerable, func_or_pattern) 与えられた関数やパターンに一致する要素を選んで返却。 iex> ["foo", "bar", "car", "moo"] iex> |> REnum.grep(~r/ar/) ["bar", "car"] iex> 1..10 iex> |> REnum.grep(3..8) [3, 4, 5, 6, 7, 8] Alias select(filterのalias) detect(findのalias) each_with_object(reduce/3のalias) inject(reduceのalias) include?(member?のalias) RailsのActiveSupportから移植してきたおすすめな関数達 exclude?(enumerable, element) Enum.member?の逆。elementがenumerableに含まれない場合にtrueを返却。 iex> REnum.exclude?([2], 1) true iex> REnum.exclude?([2], 2) false without(enumerable, elements) elements を除いて enumerable を返却。 iex> REnum.without(1..5, [1, 5]) [2, 3, 4] iex> REnum.without(%{foo: 1, bar: 2, baz: 3}, [:bar]) %{foo: 1, baz: 3} in_order_of(map_list, key, series) 元の map_list の要素のkeyに基づき、series で提供される順序に設定されたリストを返却。 iex> payments = [ ...> %Payment{dollars: 5, cents: 99}, ...> %Payment{dollars: 10, cents: 0}, ...> %Payment{dollars: 0, cents: 5} ...> ] iex> REnum.in_order_of(payments, :cents, [0, 5]) [ %Payment{cents: 0, dollars: 10}, %Payment{cents: 5, dollars: 0} ] pluck(map_list, keys_or_key) map_list から、与えられたkeysのvalueを返却。 iex> payments = [ ...> %Payment{dollars: 5, cents: 99}, ...> %Payment{dollars: 10, cents: 0}, ...> %Payment{dollars: 0, cents: 5} ...> ] iex> REnum.pluck(payments, [:dollars, :cents]) [[5, 99], [10, 0], [0, 5]] iex> REnum.pluck(payments, :dollars) [5, 10, 0] iex> REnum.pluck([], :dollars) [] blank?(any) any が空白、偽、空、または空白文字列である場合にtrueを返却。 例えば、nil, "", " ", [], {}, falseはtrueに該当する。 iex> REnum.Utils.blank?(%{}) true iex> REnum.Utils.blank?([1]) false iex> REnum.Utils.blank?(" ") true present?(any)はblank?の偽を示す。 その他、個人的に解せなかった部分を解消した関数達 find_index_with_index(enumerable, func) find_indexの際にfuncの第2引数としてcurrent_indexを渡せる用にした関数。 iex> REnum.find_index_with_index(1..3, fn el, index -> ...> IO.inspect(index) ...> el == 2 ...> end) # 0 # 1 1 list_and_not_keyword?(enumerable) keyword listとlistを区別する関数。 iex> REnum.list_and_not_keyword?([1, 2, 3]) true iex> REnum.list_and_not_keyword?([a: 1, b: 2]) false map_and_not_range?(enumerable) is_map(1..3) == trueなのが解せなくて作った関数。 iex> REnum.map_and_not_range?(%{}) true iex> REnum.map_and_not_range?(1..3) false REnumは既存のEnum関数もすべて滞りなく使えます。 REnumはメタプログラミングにより、,ElixirのEnumや対応するEnumerable系のfunctionsを全て移譲しています。 なので、以下動作も問題なく使うことが出来ます。 iex> list = [1, 2, 3] iex> REnum.find(list ,fn x -> rem(x, 2) == 1 end) == Enum.find(list ,fn x -> rem(x, 2) == 1 end) true iex> REnum.sort(list) == Enum.sort(list) true コード内のEnum.*をREnum.*に置き換えても問題なく動くはずです。 終わりに 実装に関しては手が空いたタイミングで少しずつ拡充していこうと思っています。現在はEnumerable系の実装が終わり、List系の実装を開始しております。 もしよろしければスター等頂けましたら幸いです。コントリビュートも大歓迎です。 ロードマップは以下通り。 - [x] 0.1.0 - REnum.Enumerable.Native - REnum.Enumerable.Ruby - REnum.Enumerable.Support - REnum.List.Native - REnum.Map.Native - REnum.Range.Native - REnum.Stream.Native - REnum.Utils - [x] 0.2.0 - REnum.Enumerable.ActiveSupport - [ ] 0.3.0 - REnum.List.Ruby - [ ] 0.4.0 - REnum.List.ActiveSupport - [ ] 0.5.0 - REnum.Map.Ruby - REnum.Map.ActiveSupport - [ ] 0.6.0 - REnum.Range.Ruby - REnum.Range.ActiveSupport - [ ] 0.7.0 - REnum.Stream.Ruby - REnum.Stream.ActiveSupport
- 投稿日:2022-01-16T11:38:33+09:00
rails newでコケたときの備忘 m1 mac macOS 12.1
書いた理由 備忘のため。 ググってもダイレクトに出てこなかったので、同じところで詰まっている人の参考になれば。 起こったこと rails new を叩いたら以下のメッセージでコケた /Users/hoge/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/gems/bootsnap-1.9.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require': dlopen(/Users/hoge/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/gems/bootsnap-1.9.4/lib/bootsnap/bootsnap.bundle, 0x0009): tried: '/Users/hoge/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/gems/bootsnap-1.9.4/lib/bootsnap/bootsnap.bundle' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64e')), '/usr/local/lib/bootsnap.bundle' (no such file), '/usr/lib/bootsnap.bundle' (no such file) - /Users/hoge/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/gems/bootsnap-1.9.4/lib/bootsnap/bootsnap.bundle (LoadError) 解決したこと railsのアップデート gem install rails 補足 rails tutorialを進めるにあたって、railsのバージョンを古いもので指定で入れていたので、 そのあたりがm1 macと折り合いが悪かったのではないかと推定。
- 投稿日:2022-01-16T08:18:00+09:00
{Rails}uninitialized constant * (NameError)と出た場合の対処法
①やりたいこと rails cでレコードを確認&作成したいが、uninitialized constant User (NameError)と出てしまう。※Userはモデル名です。 ②エラー内容 % rails c Running via Spring preloader in process 18643 Loading development environment (Rails 6.1.4.4) irb(main):001:0> User.all (irb):1:in `<main>': uninitialized constant User (NameError) ③原因 uninitialized constant * (NameError)は、Rubyで「定数やclassが定義されていない」ことを意味しています。 ファイル名の記述間違いなどで、呼びたいクラス名を記述できていない場合などで発生します。 この場合はコントローラー名が間違っていたみたいです。 ④解決策 ターミナルを遡ってみると、コントローラーを作成しているところまで来たのですが、、 usersじゃなくて正しくはUsersでした。 % rails g controller users Running via Spring preloader in process 14608 create app/controllers/users_controller.rb invoke erb create app/views/users invoke test_unit create test/controllers/users_controller_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit invoke assets invoke scss create app/assets/stylesheets/users.scss なので、間違えて作成したコントローラーを消して、新しくコントローラーを作成し直す必要があります。 調べてみたら、% rails destroy controllerで消すことができるとのこと。 % rails destroy controller users Running via Spring preloader in process 19108 remove app/controllers/users_controller.rb invoke erb remove app/views/users invoke test_unit remove test/controllers/users_controller_test.rb invoke helper remove app/helpers/users_helper.rb invoke test_unit invoke assets invoke scss remove app/assets/stylesheets/users.scss が、ここで問題発生。ちゃんと消えたか確認するためにapp/controllersを開いてみると、users_controllerがまだ表示されていました。VScodeだとこの表示消えないのかな。。 ファイルの所で右クリックしてusers_controllerだけ削除してみました。 この状態で再度 % rails g controller Usersでコントローラーを作成しようとすると、作成自体はできますが下の方に The name 'UsersHelper' is either already used in your application or reserved by Ruby on Rails. と出ます。仕方ないので users_helperも右クリックして削除項目を選びます。その後% rails g controller Usersを実行して、controllerが作成されたのを確認してから、rails cコマンドを実行。その結果が下記です。 % rails c Running via Spring preloader in process 19669 Loading development environment (Rails 6.1.4.4) irb(main):001:0> User.all User Load (0.6ms) SELECT `users`.* FROM `users` => [#<User:0x00007fafca4c9538 id: 2, name: "you", email: nil, pass: nil, image: nil, created_at: Fri, 14 Jan 2022 04:40:49.121048000 UTC +00:00, updated_at: Fri, 14 Jan 2022 04:40:49.121048000 UTC +00:00>, #<User:0x00007fafce64c690 id: 3, name: "he", email: nil, pass: nil, image: nil, created_at: Fri, 14 Jan 2022 04:41:44.970457000 UTC +00:00, updated_at: Fri, 14 Jan 2022 04:41:44.970457000 UTC +00:00>] irb(main):002:0> 無事レコード一覧が出せました! あとがき:コントローラー名の誤りという見立ては間違ってないのですが、% rails destroy controllerでちゃんと削除できなかったのだけがなんとも。VScodeだと消えないんですかね〜。。 有識者の方ご意見くださると嬉しいです。 ただ目的としていたレコードの確認はできたので、結果オーライ?ではあります。 開発環境 mac OS バージョン11.6 Rails 6.1.4.4 Ruby 3.0.3 MYSQL 0.5 エディタ VScode