20210105のRailsに関する記事は14件です。

簡単カレンダーを少し装飾してみた

はじめに

 昨日、導入したsimple_calendarについて、少し装飾を加えたので、紹介します。

簡単カレンダー

表示のされかたを変えたい

<%= month_calendar do |date| %>
  <%= date.strftime("%d") %>
<% end %>

strftim("%d")によって表示を変えることができます。ちなみに、この記述で、日にちだけの表示にできます。

曜日に色をつけたい

calendar.css
.wday-0 {
  color: red;
}
.wday-6 {
  color: blue;
}

変更前

変更後

少しはカレンダーっぽくなったかな…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

simple_calendarを導入したRails6.0でのDB環境PostgreSQLの設定

学習メモ用です。

解決したい問題

simple_calendarを導入したRails6.0で
開発環境DBがSQLite3、本番環境DBがPostgreSQL

本番環境において下記エラーが発生し、posts/index viewが表示されない事態に。
simple_calendarを使っているから?

ActionView::Template::Error (PG::UndefinedColumn: ERROR: column posts.user_id does not exist

なぜか、本番環境ではpostsテーブルにuser_id columnだけが存在しないからうまくいかない。

と、いうことで開発環境にもPostgreSQLを適用してみる!

開発環境

Ruby 2.7.1
Rails 6.0.3
OS macOS Catalina

pggemのインストール

rails6.0では

% rails db:system:change --to=postgesql

このコマンドで簡単に移行できるよう。

以下のようにsqlite3だったところがpgに置き換わる(Gemfileも同様に置き換わる)

config/database.yml
<!-- some long code -->

default: &default
    - adapter: sqlite3
    + adapter: postgresql

<!-- some long code -->

bundle installを実行すると

    [!] There was an error parsing `Gemfile`: You cannot specify the same gem twice with different version requirements.
    You specified: pg (~> 1.4) and pg (>= 0). Bundler cannot continue.

pggemのバージョン違いが同時にあるためbundleinstallできないとのこと
pg (~> 1.4)を残し、gem pgは削除して再度

    Fetching gem metadata from https://rubygems.org/.
    Could not find gem 'pg (~> 1.4)' in any of the
    gem sources listed in your Gemfile.

'pg (~> 1.4)'が見つからないとのことで、gem istall pgを実行すると

    Building native extensions. This could take a while...
    ERROR:  Error installing pg:
    ERROR: Failed to build gem native extension.

            <!-- some long code --> 

    checking for pg_config... no
    No pg_config... trying anyway. If building fails, please try again with
    --with-pg-config=/path/to/pg_config
    checking for libpq-fe.h... no
    Can't find the 'libpq-fe.h header
    *** extconf.rb failed ***
    Could not create Makefile due to some reason, probably lack of necessary

            <!-- some long code --> 

No pg_configとCan't find the 'libpq-fe.h header が問題っぽい。

まずPostgreSQLインストールをできていないのでは?

% brew install postgresql

↑これだと最新安定版をインストール可
インストール成功!

% postgres --version postgres 
(PostgreSQL) 13.1

gem install pgを実行

    Building native extensions. This could take a while...
    Successfully installed pg-1.2.3
    Parsing documentation for pg-1.2.3
    Installing ri documentation for pg-1.2.3
    Done installing documentation for pg after 1 seconds
    1 gem installed

    gem 'pg (~> 1.4)' in any of the gem sources listed in your Gemfile

gem 'pg (~> 1.4)'はインストールできなかったので、指定やめた。

再度bundle install実行、無事成功!

rails sを起動してうまくいってるか確かめようとしたところ、
いつも通りyarn installをしろとのことで、

 % yarn install --check-files
    yarn install v1.22.10
    [1/4] ?  Resolving packages...
    [2/4] ?  Fetching packages...
    [3/4] ?  Linking dependencies...
    warning " > webpack-dev-server@3.11.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
    warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
    [4/4] ?  Building fresh packages...
    [1/3] ⠈ fsevents
    [-/3] ⠈ waiting...
    error /Users/shigeyuki_yunoki/ucms_app/node_modules/node-sass: Command failed.
    Exit code: 1

       <!-- some long code --> 

    No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'.

    No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'.

    No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'.

    gyp: No Xcode or CLT version detected!
    gyp ERR! configure error 
    gyp ERR! stack Error: `gyp` failed with exit code: 1
    gyp ERR! stack     at ChildProcess.onCpExit (/Users/shigeyuki_yunoki/ucms_app/node_modules/node-gyp/lib/configure.js:345:16)
    gyp ERR! stack     at ChildProcess.emit (node:events:376:20)
    gyp ERR! stack     at Process.ChildProcess._handle.onexit (node:internal/child_process:284:12)
    gyp ERR! System Darwin 19.6.0
    gyp ERR! command "/usr/local/Cellar/node/15.5.0/bin/node" "/Users/shigeyuki_yunoki/ucms_app/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
   gyp ERR! cwd /Users/shigeyuki_yunoki/ucms_app/node_modules/node-sass
   gyp ERR! node -v v15.5.0
    gyp ERR! node-gyp -v v3.8.0
   gyp ERR! not ok 
   Build failed with error code: 1

   <!-- some long code --> 

yarnにおけるXcodeとCLTをインストール

gyp: No Xcode or CLT version detected!

これが問題らしい

% pkgutil --packages | grep CL

CLToolsを確認するが何も出力されず、

Xcodeのバージョンの問題だと思われるため、再インストール

% sudo rm -rf $(xcode-select -print-path)
Password:

macのパスワードを入力したものの反応なし

% sudo rm -rf /Library/Developer/CommandLineTools

Xcode Command Line Toolsのあるディレクトリを削除することでアンインストールする。
こちらは実行していないが、最終的にうまくいったので、Xcodeはインストールしていなかったものと思われ。

% xcode-select --install

コマンド上でXcode Command Line Toolsをインストール。

xcode-select: error: command line tools are already installed, use "Software Update" to install updates

インストール済みだと上記になる

Xcodeをインストールしてから実行すると以下が出力された。

% pkgutil --packages | grep CL
    com.apple.pkg.CLTools_Executables
    com.apple.pkg.CLTools_SDK_macOS110
    com.apple.pkg.CLTools_SDK_macOS1015
    com.apple.pkg.CLTools_macOS_SDK

% pkgutil --pkg-info com.apple.pkg.CLTools_Executables
    package-id: com.apple.pkg.CLTools_Executables
    version: 12.3.0.0.1.1607026830
    volume: /
    location: /
    install-time: 1609036372
    groups: com.apple.FindSystemFiles.pkg-group

No Xcode or CLT versionは解決した!

% yarn install --check-files
   yarn install v1.22.10
    [1/4] ?  Resolving packages...
    [2/4] ?  Fetching packages...
    [3/4] ?  Linking dependencies...
    warning " > webpack-dev-server@3.11.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
    warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
    [4/4] ?  Building fresh packages...
    ✨  Done in 184.82s.

yarn install成功!

PGSQLサーバーの起動

rails sを起動してみる

  PG::ConnectionBad (could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5432"?
    ):

PGSQLサーバーが動いていないということらしい

% brew service start postgresql
Error: Unknown command: service
% brew tap homebrew/services

このコマンドでservicesのような外部コマンドを取り込めるとのこと

    % brew services start postgresql
    ==> Successfully started `postgresql` 

PGSQLサーバーが動いた!

PGSQLでのDBの作成と権限付与

rails s起動すると

ActiveRecord::NoDatabaseError
FATAL: database "ucms_app_development" does not exist

DBが存在していない

このページで最初にあげたconfig/database.ymlの詳しい中身はこちら。

config/database.yml
        default: &default
        - adapter: sqlite3 
        + adapter: postgresql 
        username: ucms_app 追記
        password: 追記
        host: localhost 追記
        encoding: utf8
        pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
        timeout: 5000

        development:
        <<: *default
        - database: db/development.sqlite3
        + database: ucms_app_development

        test:
        <<: *default
        - database: db/test.sqlite3
        + database: ucms_app_test

        production:
        <<: *default
        - database: db/production.sqlite3
        # database: ucms_app_production  Herokuへのデプロイを考慮して削除
        # username: ucms_app default内に追記したので削除
        password: <%= ENV['UCMS_APP_DATABASE_PASSWORD'] %>

下記コマンドにより、PGSQLのDB設定にpgが導入され、sqliteが削除された。
更に設定を追記したのが上記。

% rails db:system:change --to=postgesql

これでDBが作られたわけではない。

% psql -l                                   

PGSQLのDB表示する

List of databases

Name Owner Encoding Collate Ctype Access privileges
postgres (macユーザー)     UTF8 C C
template0 (macユーザー)    UTF8 C C =c/(macユーザー)
template1 (macユーザー)    UTF8 C C =c/(macユーザー)

(3 rows)

"(macユーザー)"はユーザー毎に異なる。

DBを作成する

% createdb ucms_app_development
% createdb ucms_app_test 

% createdb ucms_app_productionはherokuで本番環境を扱うため不要

% psql -l

List of databases

Name Owner Encoding Collate Ctype Access privileges
postgres (macユーザー) UTF8 C C
template0 (macユーザー) UTF8 C C =c/(macユーザー)
template1 (macユーザー) UTF8 C C =c/(macユーザー)
ucms_app_development (macユーザー) UTF8 C C
ucms_app_test (macユーザー) UTF8 C C

(5 rows)

% psql -c 'select * from pg_user' postgres
usename usesysid usecreatedb usesuper userepl usebypassrls passwd valuntil useconfig
(macユーザー) 10 t t t t ********

(1 row)

ユーザーを作成する

% createuser ucms_app

'(macユーザー)'でpsqlを操作する

% psql -U (macユーザー) ucms_app_development

 ucms_app_development=# \du #ユーザー一覧を表示

List of roles

Role name Attributes Member of
(macユーザー) Superuser, Create role, Create DB, Replication, Bypass RLS {}
ucms_app {}
 ucms_app_development=# ALTER ROLE "ucms_app" WITH Superuser; 
 ALTER ROLE #⬆️(macユーザー)がucms_appに権限を付与。
 ucms_app_development=# \du

List of roles

Role name Attributes Member of
(macユーザー) Superuser, Create role, Create DB, Replication, Bypass RLS {}
ucms_app Superuser {}

config/database.ymlのusername:と一致させる必要がある。

% psql -c 'select * from pg_user' postgres     
usename usesysid usecreatedb usesuper userepl usebypassrls passwd valuntil useconfig
(macユーザー) 10 t t t t ********
ucms_app 16384 f t f f ********

(2 rows)

% psql -l

List of databases

Name Owner Encoding Collate Ctype Access privileges
postgres (macユーザー) UTF8 C C
template0 (macユーザー) UTF8 C C =c/(macユーザー) +
(macユーザー)=CTc/(macユーザー)
template1 (macユーザー) UTF8 C C =c/(macユーザー) +
(macユーザー)=CTc/(macユーザー)
ucms_app_development ucms_app UTF8 C C
ucms_app_test ucms_app UTF8 C C
ucms_app_test-0 ucms_app UTF8 C C
ucms_app_test-1 ucms_app UTF8 C C

(7 rows)

% createdb ucms_app_development
% createdb ucms_app_test 

本当は上記のコマンドは

% createdb ucms_app_development -0 ucms_app
% createdb ucms_app_test -0 ucms_app

として、オーナーをusername:と同様のucms_appでDB作成すれば良かったが、
どこかの段階でオーナーが切り替えられたのでよしとする。

 ucms_app_development=# ALTER ROLE "ucms_app" WITH Superuser;
 ALTER ROLE

多分ここ。

rails sすると下記になるのでmigrateを実行する

    ActiveRecord::PendingMigrationError (

    Migrations are pending. To resolve this issue, run:

            rails db:migrate RAILS_ENV=development

    ):

これでPostgreSQLのDB環境設定は完了!

gem 'yaml_db'でsqlite3のデータをpgに移行

gem 'yaml_db'
bundle exec rails db:data:dump

下記ファイルが作られる。

db/data.yml
       ーーー ar_internal_metadata:
            columns:
            - key
            - value
            - created_at
            - updated_at
            records: 
            - - environment
                - development
                - '2020-11-13 12:22:09.119176'
                - '2020-11-13 12:22:09.119176'

            ---
            posts:
            columns:
            - id
            - title
            - content
            - start_time
            - user_id
            - created_at
            - updated_at
            records: 

            <!-- some long code -->

            - - 75
                - ddd
                - ddd
                - '2020-12-18 00:00:00'
                - 1
                - '2020-12-18 12:29:39.573445'
                - '2020-12-18 12:29:39.573445'

            <!-- some long code -->


            ---
            users:
            columns:
            - id
            - name
            - email
            - seriousness
            - created_at
            - updated_at
            - password_digest
            records: 
            - - 1
                - sssss
                - s@gmail.com
                - 中等症
                - '2020-11-13 12:23:49.247847'
                - '2020-11-29 01:57:03.602324'
                - "$2a$12$/r0ip..."

            <!-- some long code -->

% bundle exec rails db:data:load

PostgreSQLにこれで移行できる。

ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR:  column "user_id" of relation "posts" does not exist
    LINE 1: ...INTO "posts" ("id","title","content","start_time","user_id",...
                                                                ^
    bin/rails:4:in `<main>'

    Caused by:
    PG::UndefinedColumn: ERROR:  column "user_id" of relation "posts" does not exist
    LINE 1: ...INTO "posts" ("id","title","content","start_time","user_id",...

なぜか、postsのuser_idがなくなっている。。。
schema.rbが書き変わってる。。。なぜだ。。。

PostgreSQL導入前

ActiveRecord::Schema.define(version: 2020_11_15_015429) do

    create_table "posts", force: :cascade do |t|
        t.string "title"
        t.text "content"
        t.datetime "start_time"
        t.integer "user_id"
        t.datetime "created_at", precision: 6, null: false
        t.datetime "updated_at", precision: 6, null: false
        t.index ["user_id", "created_at"], name: "index_posts_on_user_id_and_created_at"
        t.index ["user_id"], name: "index_posts_on_user_id"
    end

PostgreSQL導入後

    ActiveRecord::Schema.define(version: 2020_11_15_015429) do

    # These are extensions that must be enabled in order to support this database
    enable_extension "plpgsql"

    create_table "posts", force: :cascade do |t|
        t.string "title"
        t.text "content"
        t.datetime "start_time"
        t.datetime "created_at", precision: 6, null: false
        t.datetime "updated_at", precision: 6, null: false
    end

直接、PostgreSQLに:user_id columnを作る!

    ucms_app_development=# ALTER TABLE posts ADD COLUMN user_id integer;  

    ALTER TABLE


    ucms_app_development=# \d posts

                 Table "public.posts"
    Column   |              Type              | Collation | Nullable |              Default              
    ------------+--------------------------------+-----------+----------+-----------------------------------
    id         | bigint                         |           | not null | nextval('posts_id_seq'::regclass)
    title      | character varying              |           |          | 
    content    | text                           |           |          | 
    start_time | timestamp without time zone    |           |          | 
    created_at | timestamp(6) without time zone |           | not null | 
    updated_at | timestamp(6) without time zone |           | not null | 
    user_id    | integer                        |           |          | 
    Indexes:
        "posts_pkey" PRIMARY KEY, btree (id)

    (END)

これでpostsテーブルにuser_id追加完了!

    % rails db:migrate              
    == 20201115015429 AddUserIdToPosts: migrating =================================
    == 20201115015429 AddUserIdToPosts: migrated (0.0000s) ========================

再度、% bundle exec rails db:data:loadでdb/data.ymlファイルをpgのDBに!

本番環境herokuにDB適応させる!

 % heroku pg:psql           
    --> Connecting to postgresql...
    psql (13.1, server 12.5 (Ubuntu 12.5-1.pgdg16.04+1))
    SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
    Type "help" for help.


    ucmsapp...::DATABASE=> \d posts
                                            Table "public.posts"
    Column   |              Type              | Collation | Nullable |              Default              
    ------------+--------------------------------+-----------+----------+-----------------------------------
    id         | bigint                         |           | not null | nextval('posts_id_seq'::regclass)
    title      | character varying              |           |          | 
    content    | text                           |           |          | 
    start_time | timestamp without time zone    |           |          | 
    created_at | timestamp(6) without time zone |           | not null | 
    updated_at | timestamp(6) without time zone |           | not null | 
    Indexes:
        "posts_pkey" PRIMARY KEY, btree (id)

    ucmsapp...::DATABASE=> ALTER TABLE posts ADD COLUMN user_id integer;
    ALTER TABLE

開発環境と同様にuser_idを作成

% heroku run bundle exec rails db:data:load

もしかすると

% heroku run rails db:migrate

すれば良かっただけかも。

herokuに開発環境のDBをコピーするには?

    heroku pg:push ucms_app_development ucmsapp...             
    ▸    Unknown database: ucmsapp... Valid options are:
    ▸    DATABASE_URL
config/database.yml
    #You can use this database configuration with:
    #
    #   production:
    #     url: <%= ENV['DATABASE_URL'] %>

productionのurlは'DATABASE_URL'とのこと。

    heroku pg:push ucms_app_development DATABASE_URL
    heroku-cli: Pushing ucms_app_development ---> postgresql...
    ▸    Remote database is not empty. Please create a new database or use
    ▸    heroku pg:reset

    heroku pg:reset                    
    ▸    WARNING: Destructive action
    ▸    postgresql... will lose all of its data
    ▸    
    ▸    To proceed, type ucmsapp... or re-run
    ▸    this command with --confirm ucmsapp...

    > ucmsapp...
    Resetting postgresql... done
    heroku pg:push ucms_app_development DATABASE_URL               
    heroku-cli: Pushing ucms_app_development ---> postgresql...
    pg_dump: last built-in OID is 16383
    pg_dump: reading extensions
    pg_dump: identifying extension members

        <!-- some long code -->

    pg_restore: creating CONSTRAINT "public.posts posts_pkey"
    pg_restore: creating CONSTRAINT "public.schema_migrations schema_migrations_pkey"
    pg_restore: creating CONSTRAINT "public.users users_pkey"
    heroku-cli: Pushing complete.

まとめ

本番環境DBがPostgreSQLで、DBを移行することができた!
何故、postsテーブルにuser_id columnだけが存在しなくなるのかはわからなかったので、
その疑問もいつか解決できれば!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 初心者から抜け出すべく意識すること (※随時追加予定)

コードが動けばいいやの時期はそろそろ終わりたい
と思い、調べてメモする

また業務でおこなっていて指摘されることも書きます

Ruby編

メソッド

1つのメソッドに複数のロジックを書かずに別のメソッドに抽象化させる
ロジックや操作ごとにメソッドを作って、それを呼び出す形にする

例) if文で条件により操作を分岐させているときに、
2つ以上の操作をそのメソッド内に直接書くのではなく、
操作ごとにメソッドを作って、それを呼び出す形にする

定数

ロジックで用いる具体的な数字は定数に格納する

ロジックを作るときに、生の数字を書いてしまうと初見の人には伝わらない
名前をつけた定数に格納することで、その数字が何を意味するのか分かる

例) 税 TAX = 10
基本ストレージ DEFAULT_STORAGE = 5 など

Rails

Routing

ルーティングの書く順番だけではなく**ネストできるところはする::

独自URLを設定しているルーティングは
resourcesにネストして書いた方が分かりやすくなる

config/routes.rb
Rails.application.routes.draw do
  resources :posts do
    resource :likes
  end
end

Controller

ルーティングのresourcesにonlyやexpectオプションを付けることで、
どのリソースを使っているかを可視化する

クラスを継承してsuperメソッドを使う

publicメソッドはアクションのみに

処理の移動先は基本Model
Modelへの移動とリファクタリングを同時にするのが厳しそうな場合は、
アクションの中身を一旦全てModelに映してから
必要に応じてControllerに戻す、という手順を取るのも良い

Model

ビジネスロジックはmodelに書く
concerns/に置くのはあくまで複数のmodel間で共通する処理、
クラスをまたぐ処理はconcernに

同ファイル内でmodule化する
複数のクラスで使いたい処理のまとまりはconcern化して、取り込むクラスにmix-inする

複数のmodelにまたがる処理はservice層に書く

modelからデータを取り出す方法としてscopeを使う
その場合、where句を多用して条件を毎回書かなくても、scopeに定義すればメソッドのように使える

View

共通パーツ化(部分テンプレート、partial)するのと、
viewにロジックを書かない(ヘルパー、helper)

Helperメソッドに定義する
viewにはなるべくロジックを書かない、見た目に関するメソッドはヘルパーに記載する

ページごとのjsファイルに分離する
erbファイルにスクリプトタグを仕込みかかない

viewに直接styleを記述しない

js専用のclassはjs-プレフィックスをつけて区別する

その他

繰り返し使う文字列をlocaleに定義する
時刻のフォーマット等もlocaleを使い、strftimeは基本使わない
金額や距離、日付等も指定できる

なるべく短い書き方、リファクタ
→後置if、三項演算子、ぼっち演算子、nilガード、%記法など

参考記事

https://qiita.com/TOSHIMITSU_MIYACHI/items/fec069bcdf23b6be7623
https://qiita.com/d0ne1s/items/fabbc0df7c273cf04fc3
https://qiita.com/shunsuke227ono/items/60de21690238aa25e9d4

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gem bulletを入れてみた(N+1問題に自分で気づけない人のために)

何をしたか

Railsアプリを作っています。N+1問題について前々から関心はあったのですが、SQLについて学習が不足していることもあり、自分では気づけないことも多いので、N+1問題が発生している箇所があったら教えてくれるgem bulletを入れてみることにしました。

▶︎公式のドキュメントはこちら:bullet

導入方法

以下のコードをGemfileに記載します。

Gemfile
group :development do
  gem 'bullet'
end

bundle install後、以下のコマンドを実行します。

bundle exec rails g bullet:install

この時、テスト環境にもbulletを入れるかどうか聞かれると思いますが、公式のinstallのところにはdevelop環境へのインストールだけが記されていたのと、

公式のテスト環境へのインストールの記述を見てみても、なんだか色々大変そうだな(今回はそこまで時間をかける必要はない)だったので、今回は見送りました。

デフォルトで設定される内容

↑上記のコマンドで、config/environments/development.rbにデフォルトで以下の様なコードが追加されました。

config/environments/development.rb
Rails.application.configure do
  # ここから
  config.after_initialize do
    Bullet.enable        = true
    Bullet.alert         = true
    Bullet.bullet_logger = true
    Bullet.console       = true
  # Bullet.growl         = true デフォルトでコメントアウトされてる
    Bullet.rails_logger  = true
    Bullet.add_footer    = true
  end
  # ここまでが追加される。以下、同ファイルにもとから入っていた内容は省略
end

bulletの設定可能な項目一覧は←こちらにありましたが、デフォルトで入っている項目について確認すると、

  • Bullet.enable...Bulletのgemを利用可能にします。でも、何もしません。
  • Bullet.alert...ブラウザにJSのアラートを出します。
  • Bullet.bullet_logger...bulletのログファイルを出します(場所:Rails.root/log/bullet.log)
  • Bullet.console...console.logに警告を出します。
  • Bullet.growl...Growlがインストールされているときに、ポップアップの警告を出します。
  • Bullet.rails_logger...railsのログに警告を出します。
  • Bullet.add_footer...画面左下にメッセージを出します。

...なるほどです。

実際の使用画面

実際に使用すると、こんな感じで警告が出ました。
n+1のコピー.png

直し方まで教えてくれていて便利ですね^^

未解決の問題

ところで、実際のテーブルはこんな感じで。。。。
Image from Gyazo

usersとimagesの両方のテーブルに対して、N+1問題が発生していたので、この様に治したのですが...。

@posts = Post.includes(:user, :images).order(created_at: 'DESC')

実際に本当にこれでクエリが最適化しているのかは(クエリ数は減っている)、SQLをきちんと勉強しないとわからないなと思ったのでした...。

とりあえず、SQLが未熟なまま開発を進めていた身としては、ひとつ助けになりそうです:relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuにデプロイしたのにうまく反映できない時

はじめに

忘備録として記載します。

結論

「Command+z」で戻りすぎていた。

過程

デプロイした後、
heroku open
でブラウザを立ち上げたところ、なぜか反映されていない。
そこで、念のためローカルホストでまずは確認した。
すると、書いたはずのコードが消失していた。
「Command+z」で戻りすぎてしまい、消えたと思われる。

こんなミスで1時間もの間、Pushしたり、マイグレートしたりと不毛な時間を過ごしてしまったので、しっかり確認してから進めていこう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

has_one association factory_bot accepts_nested_attributes_for request_spec [自分用]

class Project < ApplicationRecord
  has_one :location, class: 'Project::Location'
  accepts_nested_attributes_for :location, allow_destroy: true
end

class Project::Location < ApplicationRecord
  belongs_to project
end

factory

FactoryBot.define do
  factory :project_location, class: 'Project::Location' do
    association :prefecture
    association :project
    address { '港区芝公園4丁目2−8' }
    station { 'JR浜松町駅' }
  end
end

request spec

    let!(:prefecture) { create(:prefecture) }
    let!(:category) { create(:project_category) }
    let(:project_params) do
      attributes_for(:project, location_attributes: attributes_for(:project_location,
                                                                   prefecture_id: prefecture.id)
    end

 post projects_path, params: { project: project_params }

prefecture_idを指定してあげる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6でMaterialize.cssのDatepickerを使いたい!

初めまして、ゆん♂です!
不動産業界に激震を走らせるためのアプリを作ってます!

どうせ作るなら最新の環境で作りたい!ということで Rails6系 でアプリ制作してます。

が! Rails5系 と Rails6系 では大きく仕様が異なっており、表題のような簡単なことでも超絶つまづいてしまいました。
解決してみたら何のことはなかったんですが、僕はこれだけで丸1日溶けました…。
なので同じ轍を踏まないよう、僕の試行錯誤の軌跡をここに残しておきます。

長ったらしい話はいいからさっさと結果だけよこせや、って方はこちらへどうぞ!

開発環境

言語: Ruby2.6.6
フレームワーク: Rails6.0.3.4
データベース: MySQL(ローカル)
IDE: VScode

Rails6ってなんかカッコイイ!!

これは Rails6 を採択した理由です。マジです。
Rails6って何か響きだけでカッコ良くないですか!?
しかも5よりも6のほうが新しいし、古いバージョンで作って早々バージョンアップデートとか必要になったら二度手間!
(↑これは多分正しい、、、はず)
という安直な理由で Rails6 を採択しました。

このあと地獄を見るとも知らずに、、、

Materialize.cssのDatepickerオシャレ!!

ちょっと見てくださいコレ。
めちゃくちゃオシャレじゃないですか??
CSS 全く書かずにこんなUIが実現できるなんて便利な世の中ですよねほんと☺️

スクリーンショット 2021-01-05 14.55.28.png

で、このドキュメントには
「カレンダー表示させたい inputタグ に datepicker というクラス名を付けてな」
「あとは JavaScript か jQuery でちょちょいと書いたら終わりやで」
と書いてあります。オージーザス、神よありがとう?

gem じゃなくて webpacker!!

Datepicker を使うには jQuery を入れておいた方が良さそうです。
Rails5系 だと gem でインストールして CSS や JS で読み込ませてねー的な記事がたくさん出てくるのですが、Rails6系 では JavaScript系のパッケージ機能を gem ではなく webpacker で管理するようになったそうです!

Webpackerとは、 Webpackを使用してRuby on Rails上でJavaScript開発をするために必要な一連のまとまりを、標準で実装することができるgemパッケージ です。 Rails6.0より、webpackerが標準実装になりました。

参照: 【基本】webpackerとは何か学ぼう

なんてこったパンナコッタ!!

オーマイゴッドファーザー降臨!よいしょ!

こりゃ便利!!、、、、、なのでしょうか???

この辺り実はメリデメがよく分かっていません。
んー、あいまいみーまいん?

まぁ正確な理解は置いておいて、Rails6 jQuery で調べると必ずと言っていいほど webpacker で管理しましょうねーという記事が出てきます。
というかそんな記事しかありません!
(↑それ自体は多分正しい)
併せて jquery-ui-dist なるものもインストールする必要があるようです。

以下、参考にした記事
Rails6でjQueryの導入方法
【Rails6】jQueryがなかなか使い始められない人へ
【Rails6】Webpackerを用いてjQueryをインストールする手順を簡単にまとめてみた
Rails 6:webpackerを介してjquery-uiを追加する方法は?

実はこの時すでに術中にハマってました。
js系のパッ毛0時は必ず webpacker で管理しなければならないと思い込んでいたのです。
そのため、 rails6 webpacker datepicker とか webpacker jquery-ui-dist などと調べていたため、余計にドツボにハマる結果となりました。

パッ毛0時 て何やねん、パッケージ やろがい。

丸1日かけてやったこと(間違い)

リアルに丸1日ハマり倒した内容を簡潔に書いていきます。
間違ってもこの手順を踏まないでください。
あくまでも同じ過ちを犯さないで欲しいがためのメモです。
これは誤った手順です!

① Yarnで jQuery と jquery-ui-dist をインストール

$ yarn add jquery
$ yarn add jquery-ui-dist

↑ターミナルでこのコマンドを実行しました。

② jQuery と jquery-ui-dist を有効化

config/webpack/environment.js
const { environment } = require('@rails/webpacker')

// 追加ここから //////////////////////////////////////////
const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
  })
);

const aliasConfig = {
  'jquery': 'jquery-ui-dist/external/jquery/jquery.js',
  'jquery-ui': 'jquery-ui-dist/jquery-ui.js'
};

environment.config.set('resolve.alias', aliasConfig);
// 追加ここまで //////////////////////////////////////////

module.exports = environment

app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

// 追加ここから //////////////////////////////////////////
global.$ = require("jquery")

require("jquery")
require("jquery-ui")
// 追加ここまで //////////////////////////////////////////
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>ReTECH</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <!-- Compiled and minified CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
    <!-- Compiled and minified JavaScript -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
    <!-- Let browser know website is optimized for mobile -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <!-- Make FontAwesome available -->
    <script src="https://kit.fontawesome.com/xxxxxxxxxxx.js" crossorigin="anonymous"></script>

    <%= favicon_link_tag 'favicon.ico' %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= stylesheet_link_tag '//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/le-frog/jquery-ui.min.css' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

↑ 各ファイルにこのような記述を追加しました。
※ app/views/layouts/application.html.erb に関しては Materialize.css をCDN読み込みしてたりするので丸々掲載しました。FontAwesome はアカウントごとにリンクが異なるので自分でアカウント発行してください。

参照: Rails 6: How to add jquery-ui through webpacker?
参照: FontAwesome6 無料登録

③ datepicker をフォームに適用してみる

app/javascript/modules/datepicker.js
$(document).ready(function(){
  $('.datepicker').datepicker();
});

↑datepicker用jsファイルを定義

app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

global.$ = require("jquery")
require("jquery")
require("jquery-ui")

// 追加ここから //////////////////////////////////////////
require("../modules/datepicker")
// 追加ここまで //////////////////////////////////////////

↑作ったdatepicker定義ファイルの読み込み

app/views/devise/registrations/new.html.erb
<div class="input-field">
  <%= f.label :birthday %><br />
  <%= f.text_field :birthday, required: true, class: 'datepicker' %>
</div>

↑誕生日入力フォームに datepicker というクラス名を付与

出来上がったものがこちらです☺️

スクリーンショット 2021-01-05 9.01.50.png

おい!

昔のiPhoneかよ!

一瞬電卓に見えたわ!

マテリアルの「マ」の字もないな!

作りたいのはこっち!!

スクリーンショット 2021-01-05 14.55.28.png

どれどれ、、、

スクリーンショット 2021-01-05 9.01.50.png

電卓かて!!

たぶん原因はこんな感じ

恐らく原因は CDNで Materialize.css を先に呼び出してしまい、そのあと webpacker の設定が読み込まれるため デフォルトの datepicker のUIで上書きされてしまっているのかなと推測しました。

ダメ元で Materialize.css のCDN呼び出しの記述をファイル後尾に移動したり、CDNではなく CSSファイル と JSファイル をダウンロードしてきて配置したりしました。

が、やはりダメ、、、

今までやったことは一応 Github でプルリク作って、もう一度やり直すことを決めました。。。

そして色んな試行錯誤を重ねた結果、こんな境地にたどり着いたのです。

jQuery もCDNで呼び出せちゃったりしたりするかもなんじゃない?

そうです、何も webpacker で管理せず、Materialize.css と同じようにCDNで呼び出しちゃえばいいのでは?
そうすれば先に jQuery 呼び出して、後から Materialize.css を呼び出せる、、、UIを上書きされることもないはず!!

ということでやってみた(これが正解)

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>ReTECH</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <!-- Expressly read CDN to make jQuery available before make Materialize-UI available -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <!-- Compiled and minified CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
    <!-- Compiled and minified JavaScript -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
    <!-- Let browser know website is optimized for mobile -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <!-- Make FontAwesome available -->
    <script src="https://kit.fontawesome.com/xxxxxxxxxxxx.js" crossorigin="anonymous"></script>
    <%= favicon_link_tag 'favicon.ico' %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

↑<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>という記述で jQuery を先に読み込み、そのあとに Materialize.css のCDNを読み込んでいる。

app/javascript/modules/datepicker.js
$('document').ready(function() {
  $('.datepicker').datepicker();
})

↑datepicker定義

app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

// 追加ここから //////////////////////////////////////////
require("../modules/datepicker")
// 追加ここまで //////////////////////////////////////////

↑datepicker読み込み

app/views/devise/registrations/new.html.erb
<div class="input-field">
  <%= f.label :birthday %><br />
  <%= f.text_field :birthday, required: true, class: 'datepicker' %>
</div>

↑誕生日入力フォームに datepicker というクラス名を付与

出来上がったものがこちらです?

スクリーンショット 2021-01-05 14.17.29.png

完璧!!!

以上、長文にお付き合いいただきありがとうございました?‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】resourcesメソッドを使って、ルーティングを自動で作成する。

はじめに

resourcesメソッドについて、備忘録として残しておきます。

resourcesメソッド

・resourcesメソッドはroutes.rbファイルの中に書き込むメソッドです。
・Railsの基本となる7つのアクションのルーティングをまとめて追加することができます。

Railsの基本となる7つのアクション

基本となる7つのアクションを以下に記載します。

アクション HTTP 役割 URL
index get リソースの一覧を表示する。 /users
show get リソースの詳細を表示する。 /users/:id
new get リソースを新規作成する。 /users/new
create post リソースを新規作成して、保存する。 /users
edit get リソースを編集する。 /users/:id/edit
update put/patch リソースを更新させる。 /users/:id
destroy delete リソースを削除する。 /users/:id

定義の仕方

まずは、resourcesメソッドを定義していない状態でルーティングを確認し、何も定義されていないことを確認します。

続いて、以下のようにresourcesメソッドを定義していきます。
※「users」の部分には作成したコントローラ名が入ります。今回はusers_controller.rbを例に行います。

routes.rb
Rails.application.routes.draw do
#resourcesメソッド定義する
  resources :users
end

定義した後にルーティングを確認すると・・・
基本的な7つのアクションのルーティングが追加されていることがわかります。

Prefix    Verb      URI Pattern                  Controller#Action

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

resourcesメソッドのオプション

ここからは、resourcesメソッドのオプションをめもしておきます。

only

7つのアクションのうち、特定のアクションのみを指定したい時に使用できます。

routes.rb
Rails.application.routes.draw do
#createアクションとnewアクションのみ
  resources :users, only:[:create, :new]
end

ルーティングを見てみると、こんな感じになっています。

          POST      /users(.:format)              users#create
new_user  GET       /users/new(.:format)          users#new

memeber

memberメソッドは、7つのアクション以外のアクションを追加することができます。
まずは、定義方法について、以下記載します。

routes.rb
Rails.application.routes.draw do
#memberメソッドの定義方法
  resources :users do
    member do
      get :following, :followers
    end
  end

このように定義することで、7つのアクション以外に以下のようなルーティングが得られます。
memberメソッドでは、idで指定した個々のリソースに対するアクションを定義できます。

Prefix            Verb      URI Pattern                       Controller#Action

following_user    GET       /users/:id/following(.:format)    users#following
followers_user    GET       /users/:id/followers(.:format)    users#followers
users             GET       /users(.:format)                  users#index
                  POST      /users(.:format)                  users#create
new_user          GET       /users/new(.:format)              users#new
edit_user         GET       /users/:id/edit(.:format)         users#edit
user              GET       /users/:id(.:format)              users#show
                  PATCH     /users/:id(.:format)              users#update
                  PUT       /users/:id(.:format)              users#update
                  DELETE    /users/:id(.:format)              users#destroy

collection

collectionメソッドもmemberメソッドと同様に7つのアクション以外のアクションを追加することができます。
memberメソッドと違う点は、collectionメソッドはリソース全体に対するアクションを定義するという点です。

定義方法もmemberメソッドと同様です。

routes.rb
Rails.application.routes.draw do
#collectionメソッドの定義方法
  resources :users do
    collection do
      get :following, :followers
    end
  end

ルーティングを見てみると、memberメソッド同様7つのアクション以外に追加されていることがわかります。
が、URI Patternの箇所が異なります。
collectionメソッドの方は、全てのリソースに対してアクションを定義しているので:/idの部分が省略されています。

Prefix            Verb      URI Pattern                       Controller#Action

following_user    GET       /users/following(.:format)    users#following
followers_user    GET       /users/followers(.:format)        users#followers
users             GET       /users(.:format)                  users#index
                  POST      /users(.:format)                  users#create
new_user          GET       /users/new(.:format)              users#new
edit_user         GET       /users/:id/edit(.:format)         users#edit
user              GET       /users/:id(.:format)              users#show
                  PATCH     /users/:id(.:format)              users#update
                  PUT       /users/:id(.:format)              users#update
                  DELETE    /users/:id(.:format)              users#destroy

【2つのメソッドの比較】

Prefix            Verb      URI Pattern                       Controller#Action

##memberメソッド
following_user    GET       /users/:id/following(.:format)    users#following
followers_user    GET       /users/:id/followers(.:format)    users#followers

##collectionメソッド
following_user    GET       /users/following(.:format)        users#following
followers_user    GET       /users/followers(.:format)        users#followers

参考文献

Rails tutorial 第7章 ユーザー登録
https://railstutorial.jp/chapters/sign_up?version=4.2

Rails tutorial 第14章 ユーザーをフォローする
https://railstutorial.jp/chapters/following_users?version=6.0#cha-following_users

【Rails】resourcesメソッドを使ってルーティングを定義しよう!
https://pikawaka.com/rails/resources

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デプロイ前にファイルの容量を確認

ファイルの容量を確認する。

AWSでECRを使用してデプロイしようとしましたがbuildとpushにとても時間がかかっていました。この原因はファイルの容量が重すぎたことでした。

du -sh ./*/

このコマンドで各ディレクトにファイルサイズが確認できます。
今は軽くなりましたが、当初はpublicディレクトが7Gほどありました。

スクリーンショット 2021-01-05 13.56.36.png

publicフォルダのpacksフォルダが原因だったのですが、これはWebpackerで生成するアセットの置き場です。

古いファイルは不要なので、デプロイする前には1度public/packsフォルダでいらないファイルを削除することをおすすめします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSpecテストでActionCableをテストしたい

前提

Rspecのセットアップなどは説明しません。
この記事などを参考にしてセットアップしてください。
https://qiita.com/tatsurou313/items/c923338d2e3c07dfd9ee

1. Capybaraのサーバーをpumaに設定する。

調べてみたところCapybaraのデフォルトのサーバはRailsアプリを別スレッドで動かしているだけのようで、ActionCableを利用するにはpumaを使う必要があります。
そのため、rails_heplerなどにpumaを使用するように記載する必要がありました。

rails_htpler.rb
Capybara.server = :puma

これでOKと思いましたが、なぜかまだテストがエラーになります。

       expect {
         fill_in "message_content", with: "宜しくお願いします。"
         click_button "送信"
       }.to change(Message, :count).by(1)

       expected `Message.count` to have changed by 1, but was changed by 0

2. sleepで待ち時間を設定する。

少し考えれば分かる簡単なことですがおそらく、非同期通信をしているのですぐにはMessageテーブルのカウントが増えませんでした。

messages_spec.rb
    expect {
      fill_in "message_content", with: "宜しくお願いします。"
      click_button "送信"
      sleep 5
    }.to change(Message, :count).by(1)

そこでこのようにsleepを入れてあげれば、無事テストはパスするようになりました。

Capybaraをpumaに変更するという記事はいくつかありましたがsleepも設定するという記事は見当たらなかったので投稿しました。

参考記事

https://qiita.com/tatsurou313/items/c923338d2e3c07dfd9ee
https://blog.willnet.in/entry/2017/06/05/092956

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでレコード登録前に確認画面を表示する

はじめに

Ruby on Rails5 速習実践ガイドのアウトプットで投稿しています。
今回は、レコードの登録前に「こちらの内容で登録します」などの確認画面を表示する機能について投稿します!例として、タスク管理アプリケーションを作成しています。

目次

  1. アクションを作成する
  2. ルーティングの追加
  3. ビューの追加と編集
  4. 戻るボタンの実装
  5. 参考文献

アクションを作成する

まずは、確認画面に遷移するアクションをcontrollerに作成します。

app/controller/tasks_controller.rb
def confirm_new
  @task = current_user.tasks.new(task_params)
  render :new unless @task.valid?
end

ルーティングの追加

今回はタスクにネストさせるため以下のように設定します。

config/routes.rb
resources :tasks do
  post :confirm, action: :confirm_new, on: :new
end

この記述によって、/tasks/new/confirmというURLが生成されます。

ビューの追加と編集

追加

まず、確認画面のビューを作成します。

app/views/tasks/confirm_new.html.slim
h1 登録内容の確認

= form_with model: @task, local: true do |f|
  table.table.table-hover
    tbody
      tr
        th= Task.human_attribute_name(:name)
        td= @task.name
        = f.hidden_field :name
      tr
        th= Task.human_attribute_name(:description)
        td= simple_format(@task.description)
        = f.hidden_field :description
  = f.submit '戻る', name: 'back', class: 'btn btn-secondary mr-3'
  = f.submit '登録', class: 'btn btn-primary'

この画面を表示するタイミングでは、データは保存されていません。このあとにcreateメソッドを実行するときにデータが必要なので、hidden_fieldで前の画面のデータをユーザーからは見えないように保持して渡すようにしています。

編集

次に、新規追加画面の遷移先を確認画面に設定しておきます。

app/views/tasks/new.html.slim
= form_with model: @task, local: true, url: confirm_new_task_path do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task-name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description'
  = f.submit "確認", class: 'btn btn-primary'

戻るボタンの実装

確認画面の戻るボタンが押されたときの処理を実装していきます。先ほど作成した確認画面の戻るボタンには、name属性に「back」をつけています。このボタンを押したときにparams[:back]というパラメータが送られるので、このparamsがあれば、newメソッドにレンダーするという流れで実装します。

app/controllers/tasks_controller.rb
def create
    @task = current_user.tasks.new(task_params)
    if params[:back].present?
      render :new
      return
    end
    if @task.save
      redirect_to tasks_url
    else
      render :new
    end
  end

以上で、確認画面の実装は終了です!

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SystemSpec導入と書き方

はじめに

これまでに引き続き、現場で使える Ruby on Rails 5速習実践ガイドのアウトプットを投稿します!
今回はrspecについてです!
タスク管理アプリケーションを例に進めていきます。

目次

  1. Rspecの準備
  2. Specの書き方
  3. FactoryBotでテストデータを作成
  4. テストを書く
  5. 参考文献

Rspecの準備

・gemをインストール
Gemfileの
group :development, :test doのブロックに以下を追記します。

gem 'rspec-rails', '~> 3.7'

記述後bundle installでgemをインストールします。完了したら、以下のコマンドを実行し、RSpecに必要なディレクトリや設定ファイルを作成します。

rails g spec:install

・testディレクトリ削除
rails newでアプリケーションを立ち上げた時に自動で作成されるtestディレクトリを削除します。なぜなら、Rspecのファイルは、specディレクトリの中に作成するからです。

rm -r ./test

・Capybaraを使うためにspec_helper.rbを編集
Capybaraはrails newをした際にインストールされているため、機能の読み込みと実行するドライバの設定を記述します。

spec/spec_helper.rb
require 'capybara/spec'
config.before(:each, type: :system) do
  driven_by :selenium_chrome_headless
end

・FactoryBotのインストール

Gemfileの
group :development, :test doのブロックに以下を追記します。

gem 'factory_bot_rails', '~> 4.11'

Specの書き方

ここでは、タスク管理アプリケーションの一覧表示に関するテストを例にします。

tasks_spec.rb
describe '一覧表示機能' do
  context 'Aさんがログインしているとき' do
    before do
      # テスト条件を満たすよう処理を記述する
    end
    it 'Aさんの投稿だけが表示される' do
      # 期待する動作を記述する
    end
  end
end

上記の例では条件が一つですが、複数ある場合はcontextをネストすることもできます。

FactoryBotでテストデータを作成

spec/factories/users.rbを作成し、Userモデルのデータを記述します。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'テストユーザー' }
    email { 'test@example.com' }
    password { 'password' }
  end
end

factory :userの記述で、railsがUserモデルのテストデータだなと、解釈してくれます。もし、違う名前をつけたいときは、以下のようにclassを明記します。

factory :test_user, class: User do

次にいま作成したユーザーに紐づく投稿データを作成します。先程と同様に、spec/factories/tasks.rbを作成します。

spec/factories/tasks.rb
FactoryBot.define do
  factory :task do
    name { 'テストを作成する' }
    description { '必要なものをインストールし、作成する。' }
    user
  end
end

上記のuserは、先ほど作成した:userのデータに紐づくものと定義しています。こちらも、モデル名と違うテストデータを紐付けるときはuserの箇所を以下のように書きます。

association :user, factory: :admin_user

テストを書く

まずは、日本語で枠組みを作成していきます。

spec/system/tasks_spec.rb
require 'rails_helper'
describe '一覧表示機能' do
  before do
    # ユーザーAを作成する
    # ユーザーAのタスクを作成する
  end
  context 'ユーザーAがログインしているとき' do
    before do
      # ユーザーAでログイン
      # ログイン画面に遷移
      # メールアドレスを入力
      # パスワードを入力
      # ログインボタンを押す
    end
    it 'ユーザーAが作成したタスクが表示される'
      # 作成されたタスクが表示されている
    end
  end
end

テストの枠組みができたら、実際にテストコードを書いていきます!

spec/system/tasks_spec.rb
require 'rails_helper'
describe '一覧表示機能' do
  before do
    # ユーザーAを作成する
    user_a = FactoryBot.create(:user)
    # ユーザーAのタスクを作成する
    FactoryBot.create(:task, name: "最初のタスク", user: user_a)
  end
  context 'ユーザーAがログインしているとき' do
    before do
      # ユーザーAでログイン
      # ログイン画面に遷移(ログイン画面のpathにvisit)
      visit login_path
      # メールアドレスを入力(labelの名称を指定します)
      fill_in 'メールアドレス', with: 'a@example.com'
      # パスワードを入力(labelの名称を指定します)
      fill_in 'パスワード', with: 'password'
      # ログインボタンを押す
      click_botton 'ログインボタン'
    end
    it 'ユーザーAが作成したタスクが表示される'
      # 作成されたタスクが表示されている
      expect(page).to have_content '最初のタスク'
    end
  end
end

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails + GraphQLでAPI作成

各バージョン

ruby: 2.7.1
rails: 6.0.3.4
graphql-ruby: 1.11.6

GraphQL Ruby

公式ページ

RailsでGraphQLを扱う場合↑のgemを使ってAPIを実装していきます。

graphiql-rails

合わせて graphiql-rails gemを入れておくとブラウザ上で実装したGraphQLの
確認ができるIDEが使えるようになります :sparkles:
※ graphql-rubyのinstall時に graphiql-rails のgemをGemfileに追加してくれます

イメージ画像
graphiql-rails

:computer:環境構築


Gemfile
gem 'graphql'
gem 'graphiql-rails' # 今回は先に入れました

gemがインストールされたら rails generate graphql:install コマンドを実行し各ファイルを生成します。
生成されたファイルは以下の通り↓

$ rails generate graphql:install
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/app_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_argument.rb
      create  app/graphql/types/base_field.rb
      create  app/graphql/types/base_enum.rb
      create  app/graphql/types/base_input_object.rb
      create  app/graphql/types/base_interface.rb
      create  app/graphql/types/base_scalar.rb
      create  app/graphql/types/base_union.rb
      create  app/graphql/types/query_type.rb
add_root_type  query
      create  app/graphql/mutations
      create  app/graphql/mutations/.keep
      create  app/graphql/mutations/base_mutation.rb
      create  app/graphql/types/mutation_type.rb
add_root_type  mutation
      create  app/controllers/graphql_controller.rb
       route  post "/graphql", to: "graphql#execute"
     gemfile  graphiql-rails
       route  graphiql-rails

この時点での routes.rb は以下のようになっています。

Rails.application.routes.draw do

  # GraphQL
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
  end

  post '/graphql', to: 'graphql#execute'
end

:pencil: 実装


Query 作成

まずは各テーブルに対応するTypeを定義しないといけないので、
例として以下の users テーブルに対応する user_type を作成してみたいと思います。

create_table :users do |t|
  t.string :name, null: false
  t.string :email
  t.timestamps
end

以下コマンドを実行すると user_type が作成されます。
(指定する型は ID がGraphQLで定義されているid用の型です(実態はString)
また語尾に ! が付いているものはnullを許容しない型となり、! が付いてないものはnull許容になります。)

$ bundle exec rails g graphql:object User id:ID! name:String! email:String

【補足】既にDBにテーブルが存在している場合はよろしくやってくれるっぽいので

$ bundle exec rails g graphql:object User

↑これでも大丈夫でした :sparkles:

生成されたファイル graphql/type/user_type.rb は以下のようになっていました。

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

既に生成されている graphql/type/query_type.rb に以下を追加します。

    field :users, [Types::UserType], null: false
    def users
      User.all
    end

http://localhost:3000/graphiql上で以下クエリを投げるとレスポンスが返ってくるかと思います。

{
  users {
    id
    name
    email
  }
}

Mutationsの作成

次にユーザーを作成するMutations CreateUser を作成してみたいと思います。

$ bundle exec rails g graphql:mutation CreateUser

graphql/mutations/create_user.rb が作成されるので、以下の様に修正します。

module Mutations
  class CreateUser < BaseMutation
    field :user, Types::UserType, null: true

    argument :name, String, required: true
    argument :email, String, required: false

    def resolve(**args)
      user = User.create!(args)
      {
        user: user
      }
    end
  end
end

既に生成されている graphql/types/mutation_type.rb に以下を追記します。

module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUser # 追記
  end
end

http://localhost:3000/graphiql上で以下を実行するとUserが作成されます。

mutation {
  createUser(
    input:{
      name: "user"
      email: "user@email.com"
    }
  ){
    user {
      id
      name 
      email
    }
  }
}

Association

  • 1:1の関連テーブルの場合

例として Post が Label と1:1で関連付されている場合

label_type.rb
module Types
  class LabelType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    ...
  end
end
module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
  end
end

↑の様に label を LabelType として定義できます。
この場合の Query のイメージとしては

{
  posts {
    id
    label {
      id
      name
    }
  }
}

上記の様に label を LabelType として必要な値をQueryできます。

  • 1:Nの関連テーブルの場合

例として User が Post と1:Nの場合

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :label, LabelType, null: true
  end
end
module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
  end
end

上記の様に posts を [PostType] として定義でき、Queryとしては

{
  user(id: 1234) {
    id
    posts {
      id
      label {
        id
        name
      }
    }
  }
}

↑の様に呼び出す事ができます。

graphql-batch

↑の説明の様に 1:1や1:Nの関連テーブルのデータも取ってくる事ができますが
今のままだとDBへの問い合わせが大量に発生してしまう場合があります。
User が Post と1:Nの場合の例で Post が100件ある場合、それぞれ100回問い合わせが発生してしまいます。

そこで解決方法の一つである複数問い合わせをまとめやってくれる graphql-batch を導入してみます。

gem 'graphql-batch'

Gemをインストールしたら、loader を作成していきます。
loader は「複数問い合わせをまとめる」部分の実装になります。

graphql/loaders/record_loader.rb
module Loaders
  class RecordLoader < GraphQL::Batch::Loader
    def initialize(model)
      @model = model
    end

    def perform(ids)
      @model.where(id: ids).each { |record| fulfill(record.id, record) }
      ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
    end
  end
end

これを先程の Post が Label と1:1で関連付されている場合に適用すると

module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
    def label
      Loaders::RecordLoader.for(Label).load(object.label_id)
    end
  end
end

こんな感じで書けます。
User が Post と1:Nの場合には別途loaderを作成します。

graphql/loaders/association_loader.rb
module Loaders
  class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)
      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      unless @model.reflect_on_association(@association_name)
        raise ArgumentError, "No association #{@association_name} on #{@model}"
      end
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end

※ loaderはgraphql-batchのリポジトリにサンプルがあるので、そちらを参考にして実装すると良さそうです

以下の様に書くと、まとめて問い合わせしてくれるようになります。

module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
    def posts
      Loaders::AssociationLoader.for(User, :posts).load(object)
    end
  end
end

スキーマファイルからドキュメント生成

最後に定義したスキーマファイルから良い感じのドキュメントを自動で生成するようにしてみたいと思います。

routes.rb にマウントできてデプロイ毎に自動でgraphdocが更新される
便利なgemを探していたらgraphdoc-rubyというgemがあったので試してみます。

Gemfile に以下を追加

gem 'graphdoc-ruby'

また、npmパッケージの@2fd/graphdocも必要なので
予めDockerイメージ内でインストールしておきます。(Docker使用してない場合はローカル環境にインストールすれば良いかと思います)

例)

RUN set -ex \
    && wget -qO- https://deb.nodesource.com/setup_10.x | bash - \
    && apt-get update \
    && apt-get install -y \
                 ...
                 --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g yarn \
    && npm install -g @2fd/graphdoc # インストールしとく

config/routes.rb に以下を追記

config/routes.rb
Rails.application.routes.draw do
  mount GraphdocRuby::Application, at: 'graphdoc'
end

※ エンドポイントを変更している場合、config/initializers/graphdoc.rb を修正する

例)

GraphdocRuby.configure do |config|
  config.endpoint = 'http://0.0.0.0:3000/api/v1/graphql'
end

Railsを再起動して、http://localhost:3000/graphdoc でドキュメントが生成されればOKです :sparkles:

graphdoc

:bomb: バッドノウハウ


  • http://localhost:3000/graphiql アクセス時に以下エラーが発生する場合

    Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
    
  • graphiqlの画面にTypeError: Cannot read property 'types' of undefined が表示される
    -> 手元の環境だとRails再起動で治りました

  • graphiqlの画面にSyntaxError: Unexpected token < in JSON at position 0 が表示される
    -> エラーが発生してる可能性がるのでログを見て修正する

:link: 参考になったURL


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

姓と名を別々に入力させて、保存する前に結合する

忘備録です。
Deviseでの新規登録
名前のフォームを姓と名に分けて入力させ、保存する前に結合させる

registrations/new.html.erb
  <%= f.label :firstName, "姓" %>
  <%= f.text_field :firstName, autofocus: true, required: true, class: 'form-control' %>
  <%= f.label :lastName, "名" %>
  <%= f.text_field :lastName, autofocus: true, required: true, class: 'form-control' %>
controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  # strong parameterで姓と名の属性(firstNameとlastName)をpermitする
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:firstName, :lastName])
  end
end
models/user.rb
  # 姓と名をDBに保存する前に結合
  before_create :create_name
  def create_name
    self.name = "#{firstName} #{lastName}"
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む