20191023のRailsに関する記事は12件です。

FactoryBotで高速に大量データを作る方法

FactoryBotで大量のデータを作りたいときどうしていますか?
FactoryBotにはcreate_listという簡単に大量データが作れる便利なメソッドがあります。
ただ、とても便利なcreate_listですがパフォーマンス観点で見ると問題があります。
この記事ではサンプルを使って挙動を確認していきます。

実行環境

Ruby: 2.6.5
Rails: 6.0.0
rspec-rails: 3.9
factory_bot_rails: 5.1.1

Models

id, name, created_at, updated_atのカラムを持ったUserモデルを使います。

db/migrate/20190914140349_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end
  end
end

RSpec

use create_list

ユーザー一覧を取得するリクエストスペックです。
100件のユーザーをbeforeで作成しています。

spec/requests/users_spec.rb
require 'rails_helper'

RSpec.describe UsersController, type: :request do
  describe 'GET /users' do
    let(:headers) { { 'CONTENT_TYPE': 'application/json', 'Accept': 'application/json'} }
    subject(:req) { get users_path, { headers: headers, params: {} } }

    context 'use create_list' do
      before do
        FactoryBot.create_list(:user, 100)
      end
      it 'get users' do
        req
        expect(response).to have_http_status(200)
      end
    end
  end
end

このテストを実行すると下記のように100件のinsertクエリが発行されます。
これはよろしくありませんね。

log/test.log
  User Create (1.7ms)  INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('Rheba Pfannerstill', '2019-10-22 04:28:53.505465', '2019-10-22 04:28:53.505465')
  User Create (6.6ms)  INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('Jacquie Schaden', '2019-10-22 04:28:53.516936', '2019-10-22 04:28:53.516936')
  --- 100件のinsert ---
  User Create (0.8ms)  INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('Shawanda Raynor', '2019-10-22 04:28:53.531153', '2019-10-22 04:28:53.531153')

use build_list + import(gem activerecord-import)

上記を回避するためにcreate_listの親戚(?)build_listを使います。
activerecordに馴染んでいる人であれば名前でわかると思いますがbuild_listはオブジェクトは生成しますがsaveはしません。
saveしないのでactiverecord-importを使ってバルクインサートします。

spec/requests/users_spec.rb
context 'use build_list + import(gem activerecord-import)' do
  before do
    users = FactoryBot.build_list(:user, 100)
    User.import users
  end
  it 'get users' do
    req
    expect(response).to have_http_status(200)
  end
end

この場合は下記のように1クエリーで登録してくれます。

log/test.log
User Create Many Without Validations Or Callbacks (1.4ms)  INSERT INTO `users` (`id`,`name`,`created_at`,`updated_at`) VALUES (NULL,'Katerine Swift','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Edward Walker','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Briana Herman','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Sima Jenkins','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Veronique Padberg','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Forrest Breitenberg','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Orville Anderson','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Elvia Osinski','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Pasquale Denesik','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Harlan Cremin','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Rudy Hoeger','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Maire Rodriguez MD','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Twanna Kulas','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Twanda Stamm','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Austin Jenkins IV','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Warren Cartwright','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Bo Denesik','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mrs. Elwood Huel','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Ms. Ashanti Robel','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mauro Crona','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Marcelina Terry','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Normand Simonis','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Lorilee Mitchell','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Jesus O\'Conner','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Zane Schowalter','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Keenan Turner','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Patricia Schulist III','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Amal Gleichner','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Lili Grimes','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Florida Tremblay','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Troy Osinski','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Frank Tillman','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Criselda Grady','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Marisa Kirlin','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Maria Morar','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Carl Boehm','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Marcy Boyer','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mr. Hester Wilderman','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Annmarie Ernser','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Kelsey Lebsack','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Malissa Kohler MD','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Kimbery Hammes','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Isaac Heidenreich','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Ned Collins','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Virgen Stracke','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Buford Mertz','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Bianca Hermiston IV','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Lillie Dickinson IV','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Dana Bosco','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Dr. Wilma Reichert','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Blair Kozey MD','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Carey Zboncak','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Madaline VonRueden','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Norine Walter','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Aura Murazik','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Francesco Brakus','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Dwain Weber','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Isidro Wintheiser','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Leslie Adams','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Jimmie Lubowitz MD','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Abe Parker','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Marty Skiles','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Trent Dooley','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Miss Jeromy Bahringer','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Doloris McDermott','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Manie Maggio','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mr. Scarlet Hessel','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Ms. Ashley Marvin','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Forest Mitchell','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Kyong McClure','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Elaine Heidenreich','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Ms. Gema Fadel','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Andrea Hilll','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Wilmer Dibbert','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Forest Kulas','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mr. Seymour Runolfsdottir','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Whitney Terry','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Noe Powlowski','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Brooke Batz','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Trent Renner','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Gus Pouros','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Jules Grimes PhD','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Micheline Dibbert','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Hayden Morar','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Ms. Deangelo Wyman','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mignon O\'Reilly','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Rickie Franecki','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Claud Walter','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Devon Romaguera','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Edmond Romaguera PhD','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mr. Lala Ortiz','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Reed Torphy','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Abram D\'Amore','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mrs. Jerrod Waters','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Jovita Baumbach','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Miss Angel Beatty','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Ted Ortiz','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Mr. Layne Block','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Dewayne Donnelly','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788'),(NULL,'Syreeta Douglas IV','2019-10-22 05:08:26.131788','2019-10-22 05:08:26.131788')

use build_list + insert_all(rails >=6.0.0)

rails 6.0.0からはinsert_allというバルクインサートしてくれるメソッドが追加されたのでimportの代わりにこれを使うこともできます。
importはactiverecord-importのgemを追加する必要がありますがこの方法であれば特にgemの追加は不要です。

spec/requests/users_spec.rb
context 'use build_list + insert_all(rails >=6.0.0)' do
  before do
    users = FactoryBot.build_list(:user, 100, created_at: Time.current, updated_at: Time.current)
    # buildで生成したオブジェクトをそのままは渡せませんが
    # .attributesで渡すことができます
    User.insert_all users.map(&:attributes)
  end
  it 'get users' do
    req
    expect(response).to have_http_status(200)
  end
end

この場合も下記のように1クエリーで登録してくれます。

log/test.log
User Bulk Insert (1.4ms)  INSERT INTO `users`(`id`,`name`,`created_at`,`updated_at`) VALUES (NULL, 'Lucille Cummings', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Collette Powlowski II', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Tomas Konopelski', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Alfonzo Hettinger', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Miss Jose McCullough', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Walton Green', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Hilary Turcotte', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Weldon Schmitt DVM', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Martin Hills IV', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ms. Royce Hudson', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Emily Jacobi Sr.', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Lonny Sporer', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Lonny Schmidt', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Marcelina Rohan', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Miss Reed Dietrich', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mrs. Cristi Rohan', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Miss Asha Walker', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Andria Lynch', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Jarrett Emard', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Gerald Kerluke', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mr. Alexis Lubowitz', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Clement Turner', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Elvin Harber', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Amos Schiller', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Everette Grant', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Fransisca Tremblay', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Micheal Spinka', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mrs. Oscar Doyle', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Britt Barton', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Cherri O\'Hara', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ulrike Fay', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ms. Vasiliki Wehner', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Robyn Jast', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mr. Geraldo Effertz', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ollie Armstrong V', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Harold Baumbach', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Rosendo Marquardt DDS', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Morris Blick', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ulysses Kshlerin', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ms. Alonso Cole', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Roberto Lind', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Claudio Runolfsson', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Alexis Ledner', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Astrid Jenkins DVM', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Arla Padberg IV', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Abel Turner', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Branden Marks', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Lionel Moen', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Nestor Legros', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mr. Chong Schamberger', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ms. Carol Denesik', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Nolan Morissette DVM', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Marquis Green', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Wilford Hauck', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Michel McDermott', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mrs. Elden Kovacek', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Bart Rohan IV', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mel Blick', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Venus Rempel', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Miss Lilla Witting', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Sharen Nikolaus III', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Margaretta Johns', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Dr. Mallory Emard', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Jeffrey Swaniawski', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Rosanne Veum', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Kathryn Pacocha', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Herminia Moen Jr.', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Dane Thompson', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Arthur Gleason', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Tiana Bogisich', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Peggie Brekke', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Kasi Bechtelar', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'James Adams', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Windy Kshlerin', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Alonso Ortiz', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Francine Walker', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Latanya Haley MD', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Yael Stanton Jr.', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Jude Hackett', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Kathleen Mills V', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Heriberto Heathcote', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Edwardo Hilll', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Noe Bins', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Sonia Hettinger', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Lon Koss', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Elana Runolfsdottir', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Julee Hirthe', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Len Miller', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Dr. Mollie Beatty', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Janet Labadie', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Reinaldo Padberg', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Yahaira Dickens', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Miss Daphine Bergnaum', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Gerald Terry', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Mariam Senger III', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Herb Kuhlman', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Devin Russel', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Ernie Stehr', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Angel Hills MD', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895'), (NULL, 'Janeth Hintz IV', '2019-10-22 05:08:25.764863', '2019-10-22 05:08:25.764895') ON DUPLICATE KEY UPDATE `id`=`id`

テストケースのパフォーマンス比較

rspecの--profileオプションで速度を比較してみました。
当然の結果ですがバルクインサートの方が圧倒的に速いことがわかります。100件でも10倍くらい速いですね。
またinsert_allよりimportの方が少し速いこともわかりました。
発行されるクエリーの実行時間はほぼ同じなので、バルクインサートするまでの内部処理で時間がかかっているのでしょう。
insert_all(, update_all, upsert_allも)が追加されてimportは使わなくなるかなと思っていたのですが、性能差がありそうなので置き換える前に調査した方が良さそうですね(調査は本題ではないので別の機会に)。

※2019/10/24追記
import vs insert_allで計測してみたところ、バルクインサートの処理だけに絞るとinsert_allの方が速かったです。
今回のinsert_allの測定ではbuild_listで生成したオブジェクトを変換する処理も含まれていたので遅くなったと思われます。

Top 3 slowest examples (2.77 seconds, 99.2% of total time):
  UsersController GET /users use create_list get users
    2.21 seconds ./spec/requests/users_spec.rb:13
  UsersController GET /users use build_list + insert_all(rails >=6.0.0) get users
    0.3268 seconds ./spec/requests/users_spec.rb:24
  UsersController GET /users use build_list + import(gem activerecord-import) get users
    0.23456 seconds ./spec/requests/users_spec.rb:35
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラー時のHerokuのlogの見方

HerokuにデプロイしたWebサイトがひらけない

Application errorが発生した。

logを開いて詳細を見ろとあるが、logの見方がそもそもわからないので調べてみた。

このようなエラーが発生した。解読してみる。

2019-10-23T12:26:25.860762+00:00 heroku[router]: at=error code=H10 
desc="App crashed" method=GET path="/favicon.ico" 
host=urog.herokuapp.com request_id=2da7814a-4064-4aa8-9424-2dc03bbd8e7c 
fwd="150.46.200.24" dyno= connect= service= status=503 bytes= protocol=https

Heroku logの読み方

ログはこのような形式で出力される。

timestamp source[dyno]: message

timestamp:ログの出力時刻
source:ログの発生源
dyno:sourceより細かなログの発生源

つまり、
このログの出力時刻は2019-10-23T12:26:25.860762+00:00 で
このログの発生源はherokuのrouterである。
そしてそれ以降がメッセージ。そのメッセージの中身を見ていきます。

つまり、このエラーはherokuのrouterあたりが原因となっている。

メッセージ前半

at:重要度(必ずinfoになる)
code:エラーコード
(詳細はここhttps://devcenter.heroku.com/articles/error-codes#h10-app-crashed)
desc:エラーに対する説明

つまり、このエラーメッセージは、
アプリがクラッシュしたエラー(H10)を示しており、
(descでも記載されている)

メッセージ後半(HTTP関連)

method: HTTPリクエストのメソッドを表す
path: HTTPリクエストの リクエストターゲットの パス部分以降を表す
host:HTTPリクエストのリクエストターゲットのホスト部分を表す
request_id:HTTPリクエストのHerokuにおける識別子を示す
fwd:HTTPリクエストを送信したクライアントのIPアドレスを表す
dyno:HTTPリクエストを処理した仮想マシンの名称を示す
service:クライアントとHTTPリクエストを処理する仮想マシンとの間のデータのやりとりにかかった時間をミリ秒単位で表す
status:HTTPレスポンスのステータスコードを示す
bytes:HTTPレスポンスの長さをバイト単位で表す。

つまり、
urog.herokuapp.com/favicon.ico
に対してHTMLのGETメソッドを送ったときにエラーがでているのがわかる。

エラーの理由はサービスが停止していることだとわかる(503)。

結果、よくわからないが
heroku run rake db:migrate
したら動くようになり解決

ーーーーー
後日談
Railsの標準DBはSQLite

Herokuの標準DBはPostgreSQL

Herokuで動かすためには本番環境のDBをSQLiteからPostgreSQLに変えないといけない。
そのために3つの手順が必要↓こちらを参考に
https://qiita.com/hmmrjn/items/e2dff8036fbbd74f049a

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

【メモ】Rails APIモード × MySQL5.7 × Docker で環境構築

はじめに

久しぶりにDockerを作成したら所々躓いたので要点をメモ

手順

  1. ディレクトリ用意&移動
  2. Gemfile, Gemfile.lock, Dockerfile, docker-compose.ymlを作成
  3. 2.のファイルにコード記述
  4. $ docker-compose build
  5. $ docker-compose run app rails new --api . --force --no-deps --database=mysql
  6. config/database.ymlを修正
  7. $ docker-compose build --no-cache
  8. $ docker-compose up
  9. $ docker-compose run app rails db:create

image.png

手順3, 6で用意するファイルについて

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

source "https://rubygems.org"
gem 'rails', '6.0.0'
Dockerfile
FROM ruby:2.6.5
ENV LANG C.UTF-8

# install required libraries
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

RUN gem install bundler -v 2.0.2

# install bundler
RUN gem install bundler

WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install

WORKDIR /app
COPY . /app
docker-compose.yml
version: '3'
services:
  mysql:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      MYSQL_DATABASE: app_development
      MYSQL_USER: root
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
  app:
    tty: true
    stdin_open: true
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - mysql
volumes:
  mysql-data:
    driver: local
config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: mysql

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

# 一例です。productionについてはデプロイする際に注入する環境変数を適宜用意して下さい。
production:
  <<: *default
  username: <%= ENV['MYSQL_USER'] %>
  password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>
  database: <%= ENV['MYSQL_DATABASE'] %>
  host: <%= ENV['MYSQL_HOST'] %>
  socket: <%= ENV['MYSQL_SOCKET'] %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

devise_token_authで、Email以外でも認証を可能にする方法

deviseのデフォルトでの認証はメールアドレスのみです。

今回は、「メールアドレス」または「電話番号」、どちらでも認証を行えるよう実装したいと思います。

Userモデルに認証のためのキーを追加する

class User < ActiveRecord::Base
  extend Devise::Models
  devise :database_authenticatable, :registerable,
         :rememberable, :trackable, :validatable,
         authentication_keys: [:login]
  include DeviseTokenAuth::Concerns::User

  # ...

end

authentication_keys: [:login]にて、 認証のためのキーを:emailから、:loginに変更します。

クライアント側からは、loginキーに「電話番号」または「メールアドレス」を入れて送ります。

Sessionsコントローラーを変更する

DeviseTokenAuth::SessionsControllerのcreateアクションをオーバーライドしてます。

class Api::V1::Auth::SessionsController < DeviseTokenAuth::SessionsController

  def create
    field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
    @resource = nil

    if field
      q_value = get_case_insensitive_field_from_resource_params(field)

      @resource = find_resource(:email, q_value) if q_value.match?(/@/)
      @resource = find_resource(:phone_number, q_value) if q_value.match?(/\A\d{10}$|^\d{11}\z/)
    end

    if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
      valid_password = @resource.valid_password?(resource_params[:password])
      if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
        return render_create_error_bad_credentials
      end
      @token = @resource.create_token
      @resource.save

      sign_in(:user, @resource, store: false, bypass: false)

      yield @resource if block_given?

      render_create_success
    elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
      if @resource.respond_to?(:locked_at) && @resource.locked_at
        render_create_error_account_locked
      else
        render_create_error_not_confirmed
      end
    else
      render_create_error_bad_credentials
    end
  end

  #...

  private

  def resource_params
    params.permit(:password, :login)
  end

end

理想はsuperで呼び出して処理したいんですが、今回は無理でした。

なのでcreateメソッドを作り、そこに親のメソッドの処理を貼り付けて、必要な処理を追加という形にしてます。

主要なところだけ解説します。

ストロングパラメータでの受け取り

def resource_params
  params.permit(:password, :login)
end

見ての通りです。deviseのストロングパラメータをオーバーライドしましょう。

積集合を取る

field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first

resource_params.keys.map(&:to_sym)は、送られてきたパラメータのkeyをチェックし、配列にします。たとえば、値は[:password, :login]とかになります。

resource_class.authentication_keysは、先程設定した認証のためのkeyで、つまり[:login]です。

&でこの2つの積集合を取り、([:login]となります。)firstメソッドで、その中身を取っています。

つまり field => :loginです。

fieldがあればあとはDBまで潜りに行くだけ

    if field
      q_value = get_case_insensitive_field_from_resource_params(field)

      @resource = find_resource(:email, q_value) if q_value.match?(/@/)
      @resource = find_resource(:phone_number, q_value) if q_value.match?(/\A\d{10}$|^\d{11}\z/)
    end

get_case_insensitive_field_from_resource_params(field)は、resource_params[login]にスペースがあったら削除してくれるメソッドです。:emailキーの場合は、downcase!してくれますが、今回は:loginキーにしているので関係ないです。

あとはfind_resourceメソッドで入力された情報に合うユーザーがいるか探しにいきましょう。(クエリの発行)

ヒットしたら@resoucerにそのユーザーを返します。

その後の処理は、もともとの処理と変更ありませんので、解説はしません。

これで、「メールアドレス」または「電話番号」での認証について、成功です。

参考

(公式ドキュメント)
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address

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

railsのルーティングからOpenAPI(V3)ドキュメントを自動生成・管理するツールを作成し、4ヶ月間会社で運用した話(開発秘話もあるよ)

はじめに

railsを使ってAPIを開発している方で「 APIはあるけどドキュメントがない 」「 ドキュメントを書くための時間が確保しにくい 」こういう問題を抱えている人に役に立つかもしれない話です。

railsを使ってAPIを開発している会社でドキュメントも一緒に開発している会社って少ないと思います。 限られたリソースを上手く使ってスケジュール通りに作るために後回しにされる現実がある と思います。

この記事で紹介するツールを使えばOpenAPI(V3)形式のAPIドキュメントを書き始めるまでの環境がすぐに整います。
OpenAPIドキュメントの書き方に関してはあんまり言及しません。他の記事を参考ください。

$ bundle exec rake routes:oas:docs    # ドキュメント生成
$ bundle exec rake routes:oas:ui      # ドキュメント閲覧
$ bundle exec rake routes:oas:editor  # ドキュメント編集
$ bundle exec rake routes:oas:monitor # ドキュメント監視
$ bundle exec rake routes:oas:dist    # ドキュメント配布
$ bundle exec rake routes:oas:clean   # ドキュメント清掃
$ bundle exec rake routes:oas:analyze # ドキュメント分解・分析
$ bundle exec rake routes:oas:deploy  # ドキュメントデプロイ

これから紹介するツール(r2-oas)を使って、約500個のエンドポイントがあるAPIのドキュメントを4ヶ月、6人で書いてきました。(ツール開発は仕事以外でやりました。個人開発です。)

  • 目標をどのように立て、どれくらい達成できたのか?
  • なぜ目標を達成できなかったのか?
  • 複数人でAPIドキュメントを書くには何が重要なのか?
  • 開発秘話
  • ツールでk8s・moneyforward・leadedeskのドキュメントを分解した話
  • ツールの特徴・機能

などが少しでも書けたらいいかなって思います。

4ヶ月の実績

※会社の許可をとりデータを載せております。

全体の実績

赤線が目標ライン。8/5(月)の週の踊り場はチームで話し合って、全体的な見直しをしようと決めていた日でしたが結局何もしませんでした。

目標は、25個/週 です。

全体の実績.png

各週の実績

8/12(月)・8/19(月)の週に進捗がないのは、夏季休暇を取る人が多かったからです。私自身、8/12(月)の週は夏季休暇取りました。
最初は勢いがあったのですが、後半は僕ともう一人誰かくらいしか書いてくれなかったです。(忙しそうだったのでお願いもしきれませんでした。)

各週での実績.png

個人の実績

累積値を表示してます。日付のセルに▲のマークが隅に付いてますが、上期評価の際に振り返るために、何があったか簡単にメモを書いてました。最後の平均値ですが、6/3(月)の週と8/5(月)の週を除いた16週で平均を取ってます。

私は言い出しっぺなので当然目標 ほぼ 達成です。(達成じゃないんかい!)

個別の実績(週表).png

進捗管理表

これまで乗せてきたグラフや表はこの進捗管理表から自動生成されてます。
pathsファイル というのがrailsで言うところのコントローラーに対応してます。
後で紹介しますが、r2-oas は「コントローラー毎にAPIドキュメントが書ける」特徴があります。

進捗表.png

レビュー体制

レビューは私が一人で行いました。複数人でレビューしている時間なんてありませんでした。
そんなに厳しく見るのではなく以下の点に注意して見ました。

レビュー修正してもらってる時間はなかったので私がやってました。

  • SwaggerUIで開いてエラーにならないか
  • SwaggerEditorで開いてエラーにならないか
  • タイポしてないか
  • レスポンスのschemaやexampleが適切か
  • 考えうるクエリパラメーターはちゃんと書いてあるか
  • HTTPステータスは間違えてないか
  • 一部未完成状態ならsummaryに [TODO] が付いているか
  • .paths ファイルの更新(後から説明します。)

です。最初に雛形を一気に作成するので、ドキュメントの 構造 がしっかりしているかをレビューする必要がなかったです。

進捗報告

こんな感じで毎週月曜日にslackで進捗を報告し、「今週も頑張らなきゃって」思わせるようにしてました。
ワンポイントアドバイスとかも書いてました。メンバーが「今週こそは頑張らねば」って思ったかどうかは知りません。(笑)

slackでの告知.png

まとめ

チームメンバー6人で4ヶ月APIを書いてみた感想は、最初から感じてましたが普段仕事をしながら優先度が低くなりがちなAPIドキュメントを書いていくのは難しいなと思いました。 私はAPIドキュメントを書きながら、 ツールの問題点や改善点を解決していくと言う別の楽しみ がありましたので続けれましたが、メンバーは違ったかと思います。4ヶ月間文句もあまり言わずに書いてくれたメンバーには感謝しかないです。

一方、APIドキュメントを書いて得られた効果の方ですが、以下のようなものがあったかと思います。

  • 軽微な不具合のあるAPIを見つけることが出来た。
  • 使われてないAPIを見つけることが出来た。
  • 非推奨のAPIにマークをつける事が出来た。
  • APIがどんな事が出来るのか全体像が分かるようになった。

まぁ一般的です。

最後に、複数人でAPIドキュメントを開発していく時に必要な事ですが、便利なツールや時間もそうなんですが、一番必要なのは リーダーの魅力 なんかなって思いました。「この人が言うなら頑張ろう」「この人のために頑張りたい」そういう人徳がある人がリーダーを務めるべきかなって思いました。プロジェクトXとか下町ロケットとか見て思ってましたが、上手くいくプロジェクトって リーダーに魅力 があって、自然に周りの人が動いてるんですよね。(ドラマやバラエティなのでどこまで本当かは分かりませんが、嘘とわかってても引き込まれてしまう感がありますよね。)

まぁこれも一般的な意見ですよね。
どんなにツールを便利しても、使って書いてくれなきゃ意味がないのです。

最後に実績まとめを載っけておきます。

項目 実績/目標
チーム合計(個) 217/421
チーム平均(個/週) 13.5/25
個人合計(個) 77/85
個人平均(個/週) 4.8/5

※進捗管理表から抜け落ちてるエンドポイントがあり全体は約500個くらいあります。

簡単なチュートリアル

さて、ここら辺まで読んだらどんな感じのツールか試したくなったでしょう。

モノは試しという事でまずは触ってみましょう。

rails6でAPIドキュメントを作ってみましょう。

SwaggerUIやSwaggerEditorで開いたりする場合は以下の準備が必要です。

$ brew cask install chromedriver
$ docker pull swaggerapi/swagger-ui:latest
$ docker pull swaggerapi/swagger-editor:latest

rails newしたら、Gemfileのdevelopmentに r2-oas を追加してください。

group :development do
 gem 'r2-oas'
end

準備

$ rails _6.0.0_ new example-600 -d mysql --skip-bundle
$ cd example-600
$ bundle install --path vendor/bundle
$ # mysql2のエラーが出るときは以下を実行して、bundle installをやり直す。
$ # bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"
$ #
$ #scaffoldで適当にルーティングを作成する。
$ bundle exec rails g scaffold user name:string age:integer
$ bundle exec rails g scaffold task status:string content:string
$ bundle exec rails g scaffold Api/V1/Account status:string content:string
$ bundle exec rails g scaffold Api/V2/CustomPost status:string content:string
$ # 一旦コミットする
$ git init
$ git add . && git commit -nm "initial commit "
$ # OpenAPI(V3)形式のドキュメントの雛形生成
$ bundle exec rake routes:oas:docs
# ドキュメントはコロコロ変わるのでgitignoreに追加する。
$ echo 'oas_docs/oas_doc.yml' >> .gitignore
$ # 一旦コミットする
$ git add . && git commit -nm "generate docs"

生成したドキュメントの編集・表示

$ # SwaggerEditor(UI)で開く
$ bundle exec rake routes:oas:editor(ui)
$ # SwaggerEditor上で適当に編集して「Ctrl+C」でSwaggerEditorを閉じる。
$ # ファイルに差分が出ることを確認する。
$ #
$ # こんな風にpathsファイル毎に開くこともできます。環境変数のPATHS_FILEを指定します。
$ PATHS_FILE=oas_docs/src/paths/api/v1/task.yml bundle exec rake routes:oas:editor

こんな感じの画面が開くと思います。

Kobito.lwaN6Q.png

SwaggerEditorを操ってるgif

こんな感じでファイルが自動生成されている事が確認できるかと思います。

このようにファイル分割する事で、srcディレクトリにあるyamlから必要な分だけAPIドキュメントを作成する事を可能にしてます。

$ tree oas_docs
oas_docs
├── src
│   ├── components
│   │   ├── requestBodies
│   │   │   ├── activestorage
│   │   │   │   ├── direct_upload.yml
│   │   │   │   └── disk.yml
│   │   │   ├── api
│   │   │   │   ├── v1
│   │   │   │   │   └── task.yml
│   │   │   │   └── v2
│   │   │   │       └── post.yml
│   │   │   └── user.yml
│   │   └── schemas
│   │       ├── activestorage
│   │       │   ├── blob.yml
│   │       │   ├── direct_upload.yml
│   │       │   ├── disk.yml
│   │       │   └── representation.yml
│   │       ├── api
│   │       │   ├── v1
│   │       │   │   └── task.yml
│   │       │   └── v2
│   │       │       └── post.yml
│   │       └── user.yml
│   ├── external_docs.yml
│   ├── info.yml
│   ├── openapi.yml
│   ├── paths
│   │   ├── active_storage
│   │   │   ├── blob.yml
│   │   │   ├── direct_upload.yml
│   │   │   ├── disk.yml
│   │   │   └── representation.yml
│   │   ├── api
│   │   │   ├── v1
│   │   │   │   └── task.yml
│   │   │   └── v2
│   │   │       └── post.yml
│   │   └── user.yml
│   ├── servers.yml
│   └── tags.yml
└── oas_doc.yml

(※長いので一部省略。)

.pathsファイルの役割

.pathsファイルに、pathsファイルのpaths以下の相対パスを書くと、PATHS_FILE の環境変数が指定されてない場合は、.pathsファイルに書いた分だけAPIを表示・編集する事ができます。

$ # .pathsファイルにapi/v1/task.ymlを追加
$ echo 'api/v1/task.yml' >> ~/oas_docs/.paths
$ # SwaggerEditor(UI)で開く
$ bundle exec rake routes:oas:editor(ui)

こんな感じのツールです。

どうでしょうか?

使い方がかなり直感的な感じしないでしょうか?

開発秘話

簡単なチュートリアル をやる事で r2-oas の雰囲気は掴めたかと思います。次に開発までの話をしたいと思います。開発までの話は面白いです。どんな問題にぶち当たってどう解決してきたかが分かるからです。

私が現在の会社に転職して最初に任された仕事がrailsで作られたAPIの修正のPRのレビューでした。railsのアクションを見る必要があったのですが、約130行くらい書いてあって、何をやってるのかわからんと思ってメンバーに「APIドキュメントないですか?」と聞いたのが全ての始まりでした。メンバーは「ない。欲しいとは思ってるんだけど...」と答えました。APIドキュメントが無いと聞いて私はがっかりするのかと思ってましたが、内心少し喜んでた気がします。前職で他のチームがSwaggerでAPIドキュメントを書いたとか話を聞くたびに、「いいな!俺もやってみたいな!」って思ってたからだと思います。そんなこんなもあって、私は軽い気持ちで「じゃあ作りましょうよ!」といって世の中のエンジニアがどんなツールを使って開発しているかを調査し始めました。

  • gem 系では、swagger_blocksやrswagやautodoc、もしgrapeを使ってAPIを作成していたら、grape-swagger

  • クラウドサービス 系では、SwaggerHub・Restlet・APIMATIC・Amazon API Gateway

などがありました。

クラウドサービス 系は有料で実際使うとなると決裁しないといけなく面倒でした。かといってdslを覚えなきゃいけないしコードを書かないといけないgem系も微妙。dslがないautodocは素晴らしいツールだが、リクエストspecがしっかり書いてある事前提で効力を発揮するものでした。残念ながらリクエストspecは書いてあるがカバレッジは低かった事、それにマークダウン形式ではなくSwaggerUIで実行できるものにしたかったので採用を見送りました。

色々なツールを試しているとある共通の問題にぶち当りました。。どのツールを使っても、 ゼロからAPIドキュメントを書かなきゃいけない という問題でした。エンドポイントは 約500個 もある。日々色んな仕事をこなしながら ゼロから書いていくのは途方もなく辛いなと思いました。

一通りツールを調べて、「ゼロからAPIドキュメントを書くのは辛いですね」みたいな話をしてたら、メンバーが「 railsなんだからルーティング情報から雛形とかできないかな? 」みたいな事を呟きました。私は「これだ!」と思って、その日家に帰ってrailsのルーティング情報をなんとか取得する方法はないか調べました。

Qiitaに rails routes で出しているルーティング情報のコードベースでの出し方を紹介している記事を見つけ、それをヒントにコードを読みました。意外と簡単に取得できる事がわかって、1週間でプロトタイプを作ってみました。

最初に作ったプロトタイプでは、railsのルーティング情報を整形して、OpenAPI形式にしてドキュメントに吐き出すところまでだったと思います。試しにメンバーに見せたら、「SwaggerUIやSwaggerEditorで簡単に開きたいね」みたいな感想だったので(自分も感じてた)、それから1週間くらいかけてSwaggerUIとSwaggerEditorで開けるように作成しました。

ちょうどこの頃、DockerでSwaggerEditorを立ち上げAPIドキュメントを編集する時、そのyamlファイルはローカルストーレジ(5MB)に設定されるという事を知りました。5MBより大きいドキュメントが編集できないのはまずいなって思って、PATHS_FILE(旧名: UNIT_PATHS_FILE_PATH) という環境変数を指定して 1コントローラー毎にAPIドキュメントを編集できるよう にしました。
(PATHS_FILE=oas_docs/src/paths/v1/task.yaml bundle exec rake routes:oas:editor のような使い心地です。)

とりあえず作ったものを会社のコードで試してみたら、いい感じに動いたのでこれはいけると思って、 このツールを使って書きたい と思うようになりました。しかし、他に優先するべき仕事が入ってきて、すぐに書き始めることはありませんでした。

それから1ヶ月半くらい立ってちょうど会社の目標を立てなきゃいけない時期にメンバーとの話し合いで「 APIドキュメント作成 」目標に入れよっかとなりました。そこで初めてメンバーに作成したツールをプレゼンしました。そこから開発がスタートします。最初のツールは $ref を再帰的に読み込んでくれない程度のツールでした。

ツールの名前は「 routes_to_swagger_docs だよ。」って言ったら、「routes_to_open_api の方がよくない?」と指摘されたのですが、ごもっともでした。(最初につけた名前は routes_to_swagger_docs でした...長すぎました。)

最初の1週間は「 どのようなルールを決めて書いていくようにしたら楽なのだろうか? 」というのを考えてました。しばらくAPIドキュメント書いていると、HTTPステータス毎にレスポンスが違うのだから、そのレスポンスに対応した components/schemas がいるなと感じ始めました。components/shemasは一般的には再利用される前提で使うものですが、微妙にレスポンス形式が違い、再利用するのは混乱の元になるからやめようと思いました。(ざっと見積もって500個〜高々1000個近くはcomponents/schemasが必要でした。)

しかしこのように決意した時にcomponents/schemasの名前をHTTPメソッド・HTTPステータス毎にユニークにつけなければならない問題にぶち当たりました。つまり、

GET /v1/tasks/{id} の 200の時のレスポンスを表すcomponents/shemas名をV1_Task_P1_GET_200とする。

といった要領で名前をつける必要がありました。
(このルールは実際採用したルールです。_はネームスペースを表現したつもりです。)

こんな感じで名前をつけていけば、まぁ名前が衝突することはないだろうって思ってましたが、 メンバーにルールを守ってもらう 事を強要する事になりました。「P1って書いてあるのはpathパラメーターが一個って意味ね」みたいな説明をしてルールブックまで作ってルールを守ってもらうように心がけました。ですが、 仕上がってくるドキュメントはルールが守られてませんでした。

名前の統一感が欲しかった私は自分でレビュー修正してでもルールに合致するようにしてました。しかし、やがて限界がきます。「 なんでルール守ってくれないんだろう 」って考えました。たどり着いた結論は「 ルールがあるのがよくない 」でした。つまり、 components/schemas名も自動で設定できるようにする必要があるなと感じました。 もっとドキュメント生成時に動的に処理がしたい。 フック (before_createとかafter_createとか)の開発に取り掛かりました。

だが、問題にぶつかります。ディレクトリの階層が深いところで、なるべくrailsが適用してくれるメソッド(define_callbacksとか)を使いたくない。「だがどうやってフック作る?」フックなんて自作した事なかったし本当に悩みました。もう諦めて「define_callbackとか使おうかな」っと思いながら、昔vueで作った別のツールを触っていた時、vuex-orm の事を思い出しました。vuex-orm はvuexでリレーションを持った複雑なデータ構造を上手く扱うためのツールです。(railsで言えば、has_manyとかhas_oneとかを使えるようにしてくれます。)typescriptで書かれてます。自分は、興味を持ってこのツールのドキュメントを過去に読んでました。そこで 「beforeCreateってものがあったな」っていうのを思い出して、実装がどうなっているのかコードを読みました。意外と簡単な仕組みだったのですぐにパクって実装しました。これでフックが完成しました。(参考にしたコードはここら辺)

こんな感じの要領で使えます。

例えば全てのエンドポイントにvalidateというクエリパラメーターを設定したい場合はこう書きます。

class RtsdPathItemObject < R2OAS::Schema::V3::PathItemObject
  after_create do |doc, path|
    doc.keys.each do |verb|
      doc[verb]["parameters"] ||= []
      doc[verb]["parameters"].push({
        'name' => 'validate',
        'in' => 'query',
        'description' => 'validationモードか否か'
        'schema' => {
          'type' => 'boolean'
        }
      })
      doc[verb]["parameters"].uniq!
    end
  end
end

components/schema名も同じ要領でこうやります。こっちは単にcomponents_schema_nameメソッドのオーバーライドですね。

module Components
  class RtsdSchemaObject < R2OAS::Schema::V3::Components::SchemaObject
    # e.x.)
    # GET(200) /v1/tasks/{id} => V1_Task_P1_GET_200
    def components_schema_name(doc, path_component, tag_name, verb, http_status, schema_name)
      path_parameters_count = path_component.path_parameters.count
      excluded_path_parameters = path_component.path_excluded_path_parameters
      excluded_path_parameters_arr = excluded_path_parameters.split("/").delete_if(&:empty?)
      base_schema_name = excluded_path_parameters.split("/").map(&:singularize).map(&:camelize).join("_")

      if excluded_path_parameters.eql? "" || excluded_path_parameters_arr.count == 1
        base_schema_name = tag_name.split("/").map(&:singularize).map(&:camelize).join("_") + base_schema_name
      end

      if path_parameters_count.zero?
        "#{base_schema_name}_#{verb.upcase}_#{http_status}"
      else
        "#{base_schema_name}_P#{path_parameters_count}_#{verb.upcase}_#{http_status}"
      end
    end
  end
end

で最後にこのクラスを使うようにする。

R2OAS.configure do |config|
  config.use_object_classes.deep_merge!({
      components_schemas_object: Components::RtsdSchemaObject,
      path_item_object: Components::RtsdPathItemObject
  }
end

このようにOpenAPIドキュメントを生成する時に使用されるクラスを利用者がオーバーライドできるようにしました。

こうやって、components/schemas(requestBodiesも同様)の名前を動的に決まったルールで生成することができ、 メンバーにルールを守ってもらう必要がほぼなくなりました。

APIドキュメントを書いていてこのcomponents/schemas名の問題が一番大きかったと思います。後の問題はそんなに大きくなかったです。

  • SaggerEditorをブラウザの❌ボタンで閉じてしまって、rubyのプロセスが編集が終わった事を理解できず、ファイルの更新が行われず、編集履歴がぶっ飛んだ。

=> 15秒(デフォルト)に1回メモリに保存するようにして回避

  • EventMachine(SwaggerEditorを開きっぱなしにするために使ってる)とMutexの相性が悪くて大量に出る警告がうざい。(log writing failed. can't be called from trap context)

=> rubyのLoggerの中でMutexが使われていたのでrubyのLoggerの実装をパクってMutexなしのLoggerを作って解決

  • SwaggerEditorが貼り付けられたjsonをyamlにコンバートしようとしている時に、15秒に1回のメモリへの保存が走った時に落ちる問題。

 => 不二の病です。直しきれてないです。仮に起きても編集履歴が失われることはないです。
 => bundle exec rake routes:oas:analyze というコマンドで解決します。

  • 急性ペットストア症候群

 => これも不二の病です。SwaggerEditorのローカルストレージが上手い事書き換わらずデフォルトで設定されているペットストアのまま立ち上がってしまう問題です。
 => あんまり続く時はeditorのコンテナを削除すると治ります。
 => 頻発はしませんが、忘れた頃に起きます。

その後は問題なく書き進めました。唯一問題があるとするなら、 メンバーがなかなかAPIドキュメントを書いてくれなかった事でしょうか? この問題だけは最後の最後までそして現在も解決できずにいます。(笑) 私に魅力がないから?(笑)

そうしてあっという間に4ヶ月経ち、現在に至ります。

4ヶ月間安定して使えたこともあって、そろそろ世の中にリリースしたいと思うようになりました。だが、「 どうやって使えるツールである事をアピールする? 」という問題にぶち当たります。最初は「4ヶ月使えたしそのままリリースしちゃえ」って思ってましたが、今思えば甘かったです。(公開されているAPIドキュメントを全然扱えなかったです。components/parametersとかサポートしてなかった。)世の中でよく知られているAPIドキュメントを扱えてこそ役にツールだろうと思って、公開されているAPIドキュメントを探しました。以下の3つのAPIドキュメントを見つけました。変換ツールがあるので、v2かv3かは大した問題ではありませんでした。見つけたドキュメントは全てV2でした。

  • kuberntes(約5.5MBもある事で有名です。行数にしたら約12万行ありました。ぎょえぇぇぇ!)
  • moneyfoward(知ってました。)
  • leaddesk(たまたま見つけました。)

この3つのAPIドキュメントを分解・分析(analyze)して扱えたら 合格 としよう思いました。
( OAS_FILE=doc.yaml bundle exec rake routes:oas:analyze が正常終了するかのテスト)

しかし残念ながら 不合格 でした。(現在は修正して 合格 の状態です。)

以下のリポジトリにドキュメントを分解した結果を置いております。

kubernetesの場合

paths キーの下にパス以外が来る場合が考慮できてなかった。

つまりこういう事

paths:
  servers:
  parameters:

みたいな場合に扱えなかった問題がありました。OpenAPI(V3)のドキュメントを見るとそのような場合はありました。
なので修正して現在では扱えるようにしました。

tagsオブジェクトがない時にエラーになる。

r2-oas はtagsオブジェクトがないと使えない問題があります。ですが、kubernetesのAPIドキュメントのようにない場合もあります。tagsオブジェクトは必須ではないので、この問題はツール側で解決する必要を感じました。具体的にはanalyzeする時に、ルートキー(pathsとかcomponentsとか)の階層に tags がないなら自動で生成するようにしました。

pathパラメーターの定義がないとSwaggerEditorでエラーになる。

SwaggerEditorで開くとpathパラメターがないよとvalidationエラーが起きました。これも頑張ればツール側で吸収しようと思えば出来ましたが、結構大変だったのでやめました。kubernetes側の問題にしました。

スクリーンショット 2019-10-12 19.14.30.png

$refの再帰的読み込みの際にstack too deepが起こるケースがあり得た。

kubernetesのcomponents/schemasにはJSONSchemaが使ってあることが分かりました。こんな感じで書いてあって見事に無限ループに陥りました。JSONSchemaPropsJSONSchemaProps を参照してます。

現在では修正済みです。

"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps": {
                "description": "JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).",
                "properties": {
                    "$ref": {
                        "type": "string"
                    },
                    "$schema": {
                        "type": "string"
                    },
                    "additionalItems": {
                        "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool"
                    },
                    "additionalProperties": {
                        "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool"
                    },
                    "allOf": {
                        "items": {
                            "$ref": "#/components/schemas/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps"
                        },
                        "type": "array"
                    },
components/securitySchemes が扱えなかった。

SwaggerUIとかの authorize ボタンの部分を作り出す役割があるパーツです。
当然、サポートしました。

components/schemas名とかに . が使ってあった。

. を使ってネームスペースを表現してありました。つまりこんな感じ

io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaPropsOrBool

これは _ より短くかけていいなって思って即サポートするようにしました。

moneyfowardの場合

components/parametersが扱えなかった。

この出来事をきっかけに、全てのcomponentsオブジェクトをサポートしました。
ですが、以下の5つのオブジェクトのサポートは試験的です。

  • responses
  • examples
  • headers
  • links
  • callbacks
V3でサポートしてない型でエラー

V2ではサポートしていたがV3でサポートされてない type があるというエラーなどが出ました。

スクリーンショット 2019-10-12 18.47.40.png

yamlファイルで管理しているので適宜修正すればいいだけなので特に何もしませんでした。
型変換までサポートするとツールが複雑になるのでサポートしませんでした。

leaddeskの場合

pathパラメーターの定義がないとSwaggerEditorでエラーになる。

SwaggerEditorで開くとpathパラメターがないよとvalidationエラーが起きました。これも頑張ればツール側で吸収しようと思えば出来ましたが、結構大変だったのでやめました。leaddesk側の問題にしました。

V3でサポートしてない型でエラー

V2ではサポートしていたがV3でサポートされてない type があるというエラーなどが出ました。

Kobito.CObhFa.png

yamlファイルで管理しているので適宜修正すればいいだけなので特に何もしませんでした。
型変換までサポートするとツールが複雑になるのでサポートしませんでした。

まとめ

このように世の中で公開されているAPIドキュメントを実際に分解・分析できる事を確認しております。
kubernetesのAPIドキュメントが分解・分析出来た時は感動がでかかったです!

r2-oasの特徴

簡単にツールの特徴をまとめておきます。

  • すでに作成されたAPIのドキュメント作成に便利なツールである。
  • 覚えるのが辛いDSL形式を書かなくていい。
  • 複数人で開発してもコンフリクトはおきない。(.pathsファイルはおきる。)
  • 簡単に使う分では 設定なし で使える。
  • 高度な設定以外は、誰でもすぐ使える。
  • yamlファイルで管理できる。(拡張子は .yml)
  • ツールが合わないと思った時に簡単に他のツールに移れる。
  • 逆に他のツールからの移行も簡単にできる。
  • SwaggerEditor上での編集結果がローカルに反映される。

r2-oasの機能

簡単にツールの機能をまとめておきます。

  • 雛形を一気に作成できる。
  • ターミナルからサクッとSwaggerEditor(UI)を開いて書ける(見れる)。
  • 編集が重くならないように、コントローラー毎編集できる。
  • ドキュメント生成の際、フック処理で共通のクエリパラメーターをもたせたりなど自由度の高い書き方ができる。
  • 必要なコントローラーの分だけ指定してドキュメントを作成する事ができる。
  • components/schemas(requestBodies) の名前などを規則的に生成できる。
  • components/schemas(requestBodies)擬似ネームスペース をサポートしている。
    • namespace1.namespace2.Model (namespace_typeが :dot の時)
    • Namespace1_Namespace2_Model (namespace_typeが :underbar の時)

詳しくはyukihirop/r2-oasを見てください。

より高度にr2-oasを使う例

bundle exec rake routes:oas:docs でドキュメントを生成する時に

  • 共通のクエリパラメーターのvalidateを持たせる
  • 動的にcomponents/schemas(requestBodies)名を規則的に決定する

例を用意しております。

詳しくは yukihirop/r2oas-advanced-example

ツールの名前に関して

このツールは railsのルーティング情報からSwaggerドキュメントを生成するツール だから、「 routes_to_swagger_docs 」としていたのですが、サポートしているのはOpenAPI(V3)形式のドキュメントだけだし、モジュールを書くときに「RoutesToSwaggerDocs」は長いなぁと感じるようになりました。そこで短縮する事を考えて「r2sd」とか「rtsd」とかを頭に浮かべましたが、OpenAPISpecificationを「OAS」と省略する文化がある事を知ったので、

  • r2oas
  • r2-oas
  • rtoas

に候補を絞りました。

でここからが重要なんですが、「r2」と聞くと「d2」じゃないですか!「R2-D2」

真ん中の候補を選べばスターウォーズに出てきそうな感あってなんか良さそうだなって事で「 r2-oas(R2-OAS) 」になりました。

以上。

おわりに

私がこのようにツールを作成し、APIドキュメントを書いてこれたのは会社の理解があったからだと思います。
入社して間もない私のアイディアを採用し、私の試みに十分に時間を割いてくれた。おかげで楽しみながら問題解決ができ、プログラマーとしても実力が上がったような気がします。もしアイディアを採用してもらえず実験環境がなかったら、ツールを磨く事なく「 現実ってこんなもんだよな 」って気を落としていた思います。このような記事を書くこともなかったでしょう。本当に感謝しかありません。
(何かしらのツールでAPIドキュメントを書いてたとは思いますが...)

話は変わって下期が始まりましたね。

最近、部長と上期評価面談があって「目標高すぎたね(笑)」って言われました。ワンチャン行けるかなって思ってたけど結果的に、25個/週 は非常に高い目標でした。下期は上期の実績(13.5個/週)を参考に 10個/週 と目標を設定しました。

下期こそは目標を達成したいと思っております。
この結果は3月の終わりにQiitaで報告しようと思ってます。

お楽しみに!

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

(個人メモ) Rails5モデルの関連付けるとき 気をつけましょう

前提

Railsでモデルを更新するようなコードを書こうとして、思いの外ハマって変更しない関連させていたモデルもロードさせる

テーブル構造

以下簡単なテーブルを作ってみます。

Untitled_Diagram_drawio_-_draw_io.png

とりあえず書いてみる

ひとまず自分の思うがままにコードを書いていく。

# app/models/product.rb

class Product < ApplicationRecord
  belongs_to :category
  belongs_to :main_image, class_name: 'PublicImage', foreign_key: :main_image_id

  validates :name, uniqueness: true
end

いい感じにコードを書いているが。。。

しかし、問題が起きる…

既存レコードを更新してみるとログで無駄なqueryが出ているな

Product.first.update(name: "test")

  Product Load (0.7ms)  SELECT  `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
   (0.1ms)  BEGIN
  Category Load (0.6ms)  SELECT  `categories`.* FROM `categories` WHERE `categories`.`id` = 1 LIMIT 1
  PublicImage Load (0.2ms)  SELECT  `public_images`.* FROM `public_images` WHERE `public_images`.`id` = 1 LIMIT 1
  Product Exists (0.8ms)  SELECT  1 AS one FROM `products` WHERE `products`.`name` = BINARY 'black-ball' AND `products`.`id` != 1 LIMIT 1
   (0.2ms)  COMMIT
=> true

なぜかProductの更新だけのにCategoryとPublicImageもロードさせいるか
調べてみると「Rails5からbelongs_to関連はデフォルトでrequired: trueになる」ということがわかりました。

改善

required: falseにしたい時はoptional: trueと書けるようになる。
実感required: trueにしたいですが、そのまま書いてると無駄なqueryが発生されていたまま気持ち悪いです。

じゃ、以下の書き方で解決しましょう。
単純に外部キーを更新するとき、バリデーションかける。

# app/models/product.rb

class Product < ApplicationRecord
  belongs_to :category, optional: true
  belongs_to :main_image, class_name: 'PublicImage', foreign_key: :main_image_id, optional: true

  validates :name, uniqueness: true

  validates :category, presence: true, if: :validate_category_presence?
  validates :main_image, presence: true, if: :validate_main_image_presence?

  private

  def validate_category_presence?
    new_record? || category_id_changed?
  end

  def validate_main_image_presence?
    new_record? || main_image_id_changed?
  end
end

結果

簡単な改善ですが、スピードの効果はすごいです。
以下は簡単に検証して観ます

a = Time.now
1000.times{Product.first.update(name: "test")}
b = Time.now
b - a

修正前: 15.837107
修正後: 5.362079

以上

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

ActiveJobとSidekiqを組み合わせたときに、ジョブ単位のオプションはどこまで設定できるか?

背景

非同期処理

一般的なブラウザは、送信したHTTPリクエストが30秒以内に応答されない時はタイムアウトし、そのHTTPリクエストは失敗したと判定します。Webアプリケーションが長時間かかる処理を行いたい場合は、ブラウザがタイムアウトしないように、HTTPリクエストに応答を一旦返し、その後実際の処理を実行します。このような処理をリクエストの応答と実処理の完了が同期していない(同時でない)点から、非同期処理と呼びます。

非同期処理フレームワーク

非同期処理では、一つ一つの処理をジョブと呼びます。Webアプリケーションは、HTTPリクエストを受け取ると、次のように動作します。

  1. リクエストに応じたジョブを作成
  2. ジョブをキューと呼ばれる領域に保存
  3. HTTPリクエストに応答を返す

非同期処理にはワーカーという実行プログラムがあります。ワーカーは、キューを監視し、キューにジョブが保存されたら、キューからジョブを取り出し、そのジョブを実行します。

ワーカーの処理が終わる前に新しいリクエストが来た場合、Webアプリケーションは新しいジョブをキューに保存します。キューには一定数のジョブが保存でき、ワーカーは実行中のジョブが完了したら、次のジョブがないかキューを確認します。キューが存在するおかげでWebアプリケーションは、ワーカーの実行状態を気にせずにジョブを作成できます。

一般にキューに入れられたジョブは先入れ先出しで処理されます。このためキューはキューと呼ばれるます。

以下の機能をまとめて提供するプログラムを非同期処理フレームワークと呼びます。

  • ジョブを定義するフォーマット
  • キューの実装
  • ワーカーの実装
  • キューとワーカーを管理する機能

ActiveJobとSidekiq

Ruby on RailsというWebアプリケーションフレームワークには、非同期処理を実行するためにActiveJobという機能があります。ActiveJobでは、処理を実行する非同期処理フレームワークをいくつかの候補から選択することが出来ます。その1つにSidekiqがあります。

SidekiqはRubyで書かれた非同期処理フレームワークです。Ruby on Railsと関わりなく広く使われています1。Sidekiq自身には豊富な機能がありますが、ActiveJobから利用する場合にはいくつかの制限があります。その1つにsidekiq_optionsメソッドを使った、ジョブ単位のオプション設定があります。

ジョブ単位のオプション設定

Sidekiqを、ActiveJobを経由せずに、直接使う時は、Sidekiq::Workerというクラスを継承してジョブを実装します。Sidekiq::Workerにはsidekiq_optionsメソッドがあります。sidekiq_optionsを使うとジョブを登録するキューや、ジョブが失敗したときのリトライ処理の有無が設定できます。次の4項目を設定できます。

  • queue
  • retry
  • backtrace
  • pool

詳しくはAdvanced Options · mperham/sidekiq Wikiを読んでください。

命題

ActiveJobからSidekiqを使う際に、ジョブにsidekiq_optionsを設定できるでしょうか?

結論

Sidekiq 6.0.1 + Rails 6.0.1の組合せで、できるようになるらしい。

Sidekiq 6.0.1

https://github.com/mperham/sidekiq/wiki/Active-Job

As of Sidekiq 6.0.1 you can sidekiq_options in your ActiveJobs and configure the standard Sidekiq retry mechanism.

翻訳すると...

Sidekiq 6.0.1以降、ActiveJobsでsidekiq_optionsを使用して、標準のSidekiq再試行メカニズムを構成できます。

Rails 6.0.1

https://github.com/mperham/sidekiq/issues/4281#issuecomment-533840940

Oh and you need Rails master too, it needs Rails 6.0.1.

翻訳すると...

ああ、Railsのmasterブランチも必要です。Rails6.0.1が必要です。

その他の悪あがき

現時点で、ActiveJobからsidekiq_optionsは設定できませんが、ジョブ単位にいくつかの振る舞いを変更することが出来ます。

queue

ActiveJobからqueue_asメソッドを使えばジョブを登録するキューを指定することが出来ます。Sidekiqではキュー毎にジョブのチェック頻度が設定できます。これを使ってジョブ単位の優先順位を設定できます。
詳しくはActive Job の基礎 - Rails ガイドAdvanced Options · mperham/sidekiq Wikiを読んでください。

retry

リトライ回数を増やす

Sidekiqではリトライ回数を設定することが出来ます。ジョブが失敗した時に何回リトライするかを設定します。ジョブ単位の設定ではありませ。すべてのジョブで共通の設定です。
詳しくはError Handling · mperham/sidekiq Wikiを読んでください。

Sidekiqでのリトライ回数に、ActiveJobでのリトライ回数を追加できます。

Active Job · mperham/sidekiq Wiki によると

The default AJ retry scheme is 3 retries, 5 seconds apart. Once this is done (after 15-30 seconds), AJ will kick the job back to Sidekiq, where Sidekiq's retries with exponential backoff will take over.

翻訳すると...

「デフォルトのActiveJob再試行スキームは、5秒間隔で3回再試行されます。これが完了すると(15〜30秒後)、ActiveJobはSidekiqにジョブをキックバックします。Sidekiqは指数関数的なバックオフを使用した再試行を引き継ぎます。」

ActiveJobではretry_onメソッドを使ってリトライ回数を設定できます。例えば、次のジョブは3回リトライします。2

class AlwaysFailJob < ApplicationJob
  retry_on ZeroDivisionError, attempts: 3

  def perform(params)
    p "run job !!!"
    1/0
  end
end

このジョブを実行すると、ActiveJobが3回リトライしたあとで、Sidekiqは既定回数リトライをします。
つまり、ActiveJobがリトライする回数分、リトライ数を増やせます。

リトライ回数を0にする

ActiveJobはdicard_onメソッドを使って、特定の例外が起きた時に、ジョブを成功したことに出来ます。例えば、次のジョブは実行するとZeroDivisionErrorが起きますが、成功します。

class AlwaysSuccessJob < ApplicationJob
  discard_on ZeroDivisionError

  def perform(params)
    p "run job !!!"
    1/0
  end
end

ジョブは成功するので、Sidekiqはリトライしません。

backtrace

ジョブの失敗時に例外のbacktrace(Ruby以外の言語ではスタックトレースとも呼ばれます)を保存して、Sidekiqの管理UIから参照するためオプションです。このオプションはActiveJobから設定出来ません。

pool

SidekiqはRedisというインメモリデータベースをつかって、ジョブを保存するキューを実装しています。

ジョブを登録するときに使用するRedisへのコネクションプールを指定するオプションです。このオプションはActiveJobから設定出来ません。

このオプションの使い道は知りません。

Sidekiq::Workerを使う

つまりActiveJobを使うのをやめます。

RSpecでテストを書く時にActiveJob::TestHelperが使えません。3

代わりにSidekiq::Testingを使います。例えば、次のようにテストを書きます。

require 'rails_helper'
require 'sidekiq/testing'
Sidekiq::Testing.fake!

describe ExampleJob do
  it do
    expect {
      ExampleJob.perform_async nil
    }.to change(ExampleJob.jobs, :size).by(1)
  end
end

詳しくはTesting · mperham/sidekiq Wikiを読んでください。

参考


  1. たとえばRubyGems.orgからは6000万回ダウンロードされています。 

  2. retry_onはRails 5.1.0で追加された機能です。それ以前のバージョンのActiveJobを使う場合は自分で実装する必要があります。ActiveJobでリトライ制御 - Qiitaなどを参考にしてください。 

  3. ActiveJob::TestHelperの使用例が知りたい方はRSpec でキューイングした ActiveJob を同期実行する - QiitaDelayed Job Queue Adapter in RSpec with Rails 5.1 - Today I Learnedを読んでください。 

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

【mastodon】bundle install でエラーが出たのでメモ

はじめに

mastodonを読むと勉強になるということで、
bundle installしたらさっそくエラーが発生したのでメモ

Failed to locate protobuf

current directory:
/mastodon/vendor/bundle/ruby/2.5.0/gems/cld3-3.2.2/ext/cld3
.rbenv/versions/2.5.0/bin/ruby -r ./siteconf20180326-69724-1yq4plx.rb extconf.rb
Failed to locate protobuf

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/mastodon/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/cld3-3.2.2/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in
/mastodon/vendor/bundle/ruby/2.5.0/gems/cld3-3.2.2 for inspection.
Results logged to
/mastodon/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/cld3-3.2.2/gem_make.out

An error occurred while installing cld3 (3.2.2), and Bundler cannot continue.
Make sure that `gem install cld3 -v '3.2.2'` succeeds before bundling.

gem install cld3 -v '3.2.2' をやってみたけどダメで、
protobufというパッケージが必要だということで 
brew install protobuf を実行したら通った。

今度はこれが出てきた。

ERROR: could not find idn library!
current directory:
/mastodon/vendor/bundle/ruby/2.5.0/gems/idn-ruby-0.1.0/ext
/.rbenv/versions/2.5.0/bin/ruby -r ./siteconf20180326-5783-ruj5ss.rb extconf.rb
checking for -lidn... no
ERROR: could not find idn library!

  Please install the GNU IDN library or alternatively specify at least one
  of the following options if the library can only be found in a non-standard
  location:
    --with-idn-dir=/path/to/non/standard/location
        or
    --with-idn-lib=/path/to/non/standard/location/lib
    --with-idn-include=/path/to/non/standard/location/include

*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=.rbenv/versions/2.5.0/bin/$(RUBY_BASE_NAME)
    --with-idn-dir
    --without-idn-dir
    --with-idn-include
    --without-idn-include=${idn-dir}/include
    --with-idn-lib
    --without-idn-lib=${idn-dir}/lib
    --with-idnlib
    --without-idnlib

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/mastodon/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/idn-ruby-0.1.0/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in
/mastodon/vendor/bundle/ruby/2.5.0/gems/idn-ruby-0.1.0 for inspection.
Results logged to
/mastodon/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-16/2.5.0-static/idn-ruby-0.1.0/gem_make.out
An error occurred while installing idn-ruby (0.1.0), and Bundler cannot continue.
Make sure that `gem install idn-ruby -v '0.1.0'` succeeds before bundling.

これも brew install libidn したら通ったが
なぜこの2つが必要なのかは、よく分かっていないので
教えていただけら嬉しいです。

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

Rails6 のちょい足しな新機能を試す98(add_autoload_paths_to_load_path編)

はじめに

Rails 6 に追加された新機能を試す第98段。 今回は、 add_autoload_paths_to_load_path 編です。
Rails 6 では、 zeitwerk mode で不要なパスを $LOAD_PATH に追加するかどうかを設定するために add_autoload_paths_to_load_path が追加されました。
後方互換性を保つ(Rails 5以前と同じ動作にする)ため、デフォルト値は、 true になってます。

Ruby 2.6.5, Rails 6.0.0 で確認しました。

$ rails --version
Rails 6.0.0

今回は、スクリプトを書いて確認します。

Rails プロジェクトを作る

$ rails new rails_sandbox
cd rails_sandbox

config/application.rb を変更する。

add_autoload_paths_to_load_path を true にしてみます。

config/application.rb
...
module App
  class Application < Rails::Application
    ...
    config.add_autoload_paths_to_load_path = false
  end
end

簡単なスクリプトを書く

$LOAD_PATH の情報を出力するスクリプトを書きます。

scripts/load_path.rb
puts $LOAD_PATH.count
pp $LOAD_PATH

動作確認する

実行してみます

$ bin/rails runner scripts/load_path.rb
95
["/app/lib",
 "/app/vendor",
 "/usr/local/bundle/gems/turbolinks-5.2.1/lib",
...

add_autoload_paths_to_load_path を true に変更してから、再度、実行すると以下のように $LOAD_PATH に追加された PATH が増えていることがわかります。

$ bin/rails runner scripts/load_path.rb
113
["/app/lib",
 "/app/vendor",
 "/app/app/channels",
 ...

Zeitwerk を使っている場合は、 app ディレクトリの直下のサブディレクトリが autoload の対象のPATHとなるため、 $LOAD_PATH に追加する必要がありません。このため、autoload されるPATH を$LOAD_PATH に追加する必要はないので、 add_autoload_paths_to_load_pathfalse に変更することが推奨されています。

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try098_add_autoload_paths_to_load_path

参考情報

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

CentOS8にRuby on Rails 6.0(MySQL)の開発環境を作る

前提

  • windows10上のVirtual Box 6.0で構築
  • Virtual Box 6.0をインストール後から開始
  • CentOS8のisoファイル(CentOS-8-x86_64-1905-dvd1.iso)はダウンロード済み

各種バージョン

  • Virtual Box 6.0
  • CentOS 8
  • Ruby 2.6.5
  • Ruby on Rails 6.0
  • MySQL 8.x.x
  • Nodebrew 12.13.0

全体の流れ

  • Virtual Boxに仮想マシンを作成
  • OSインストール
  • CentOS 初期設定
  • Ruby・Railsのインストール
  • Nodeのインストール
  • MySQLのインストール
  • Firewallの設定
  • プロジェクトの作成&各種設定
  • サーバーの起動

Virtual Boxに仮想マシンを作成

「新規」を押下。
2019-10-22 (0).png

  • 仮想マシンの作成
    • 名前: rails_test(任意)
    • マシンフォルダー: (任意)
    • タイプ: Linux
    • バージョン: Red Hat (64-bit)

2019-10-22 (1).png

以降は任意で設定。今回はデフォルトのままで進めました。

仮想マシンが作成されたら、「設定」を押下します。

「システム」→「マザーボード」→「起動順序」のハードディスクの順序を一番上に持ってきてください。
これをしないと、OSインストール直後の再起動時にisoファイルが再読み込みされます。

2019-10-22 (11).png

「ネットワーク」→「アダプター 2」で「ネットワークアダプターを有効化」にチェックを入れ、割り当てを「ホストオンリーアダプター」に設定。

2019-10-22 (12).png

OSインストール

仮想マシンを起動、ダウンロードしたisoファイルを選択。
2019-10-22 (13).png

日本語を選択して続行
2019-10-22 (16).png

赤色の枠を付けたところを設定します。

  • 時刻と日付...アジア/東京
  • ソフトウェアの選択...最小限のインストール
  • インストール先...デフォルト
  • ネットワークとホスト名...Ethernet(enp0s3), Ethernet(enp0s8)の接続を両方ONにする。

リリースノートによると、Virtual Boxを使用した上で「ソフトウェアの選択」がデフォルトのままだと問題があるようです。

2019-10-22 (17).png

ここまでできたら、インストールの開始をします。rootのパスワードを設定します。ユーザーは後で作成するのでここでは何もしません。

2019-10-22 (22).png

CentOS 初期設定

インストールが終了して、再起動したらrootでログインしてください。

ネットワークの設定

# nmcli device status

すると、ホストオンリーアダプターが接続されていないと思うので、以下のコマンドで接続します。

# nmcli c up id enp0s8

パッケージのアップデート

# dnf -y update

※CentOS8から、yumコマンドからdnfコマンドになりました。

ユーザーの作成・パスワードの設定

# useradd -m [ユーザー名]
# passwd [ユーザー名]

パッケージのインストール

dnf install -y git gcc-c++ glibc-headers openssl openssl-devel readline readline-devel zlib zlib-devel bzip2 tar make

Ruby・Railsのインストール

作成したユーザーでログインしてください。

rbenvのインストール

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv

環境変数の設定

$ echo 'export RBENV_ROOT="$HOME/.rbenv"' >> ~/.bash_profile
$ echo 'export PATH="${RBENV_ROOT}/bin:${PATH}"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile
$ rbenv --version

最後にバージョンが表示されればインストール完了です。

Rubyインストール

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install -l
$ rbenv install -v 2.6.5
$ rbenv rehash
$ rbenv global 2.6.5
$ ruby -v

最後にバージョンが表示されればインストール完了です。(バージョンは適宜)

Rails インストール

$ gem update --system
$ gem install rails
$ gem install bundler
$ rbenv rehash
$ rails -v

最後にバージョンが表示されればインストール完了です。(バージョンは適宜)

Nodeをインストール

$ curl -L git.io/nodebrew | perl - setup
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
$ source ~/.bash_profile
$ nodebrew install-binary stable
$ nodebrew ls
$ nodebrew use v12.13.0
  ↑「nodebrew ls」で表示された、使用するバージョンを指定
$ node -v

最後にバージョンが表示されればインストール完了です。(バージョンは適宜)

MySQLのインストール

rootでログインしします。

インストール

# dnf install -y mysql-server mysql-devel

起動

# systemctl start mysqld

初期設定

# mysql_secure_installation

いくつか質問があるので、適宜選択してください。
今回は下の流れで行いました。
基本はyes、パスワードポリシーの強度は0(=8文字以上)です。

[root@localhost ~]# mysql_secure_installation

Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD component?

Press y|Y for Yes, any other key for No: y

There are three levels of password validation policy:

LOW    Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary                  file

Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 0
Please set the password for root here.

New password: [新パスワード]

Re-enter new password: [新パスワード(確認)]

Estimated strength of the password: 50 
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.

Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Success.


Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.

Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y
Success.

By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.


Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y
 - Dropping test database...
Success.

 - Removing privileges on test database...
Success.

Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y
Success.

All done!

完了したらMySQLにrootでログインしてください。

ユーザーの作成・権限付与

> CREATE USER '[作成ユーザー名]'@'%' IDENTIFIED BY '[パスワード]';
> GRANT ALL ON *.* TO '[ユーザー名]'@'%';
> FLUSH PRIVILEGES;

DB作成

アプリで使用するDBを作成しておきます。

> CREATE DATABASE [DB名];

ここまでできたらログアウトして、MySQLを再起動します。

# systemctl restart mysqld

Firewallの設定

ポートの3000番を開けておきます。これを忘れて「サーバー起動してもアクセスできない」というのがあるあるでした。

# systemctl start firewalld
# firewall-cmd --add-port=3000/tcp --zone=public --permanent
# firewall-cmd --reload

プロジェクトの作成&各種設定

rootからログインしなおします。

$ mkdir [プロジェクト名]
$ cd [プロジェクト名]
$ bundle init

作成されたGemファイルを「vi Gemfile」などで開き、railsのコメントアウトを外します。

source "https://rubygems.org"
gem "rails", "6.0.0"            ←コメントアウト(#)を外す

gemのインストール

$ bundle install
$ bundle exec rails new . -d mysql --skip-bundle
$ gem install mysql2
$ bundle update
$ bundle install

project/config/database.ymlの設定

default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: [ユーザー名]
  password: [パスワード]
  socket: /var/lib/mysql/mysql.sock
  database: [DB名]
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_unicode_ci

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

I18n言語設定(必要であれば)

・gemの追加... "rails-i18n"
・config/locales/にja.ymlファイルを作成

/project/config/application.rb(追記)
config.time_zone = "Tokyo"
config.active_record.default_timezone = :local

# 言語ファイルを階層ごとに設定するための記述
config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}").to_s]

# アプリケーションが対応している言語のホワイトリスト(ja = 日本語, en = 英語)
config.i18n.available_locales = %i(ja en)

# 上記の対応言語以外の言語が指定された場合、エラーとするかの設定
config.i18n.enforce_available_locales = true

# デフォルトの言語設定
config.i18n.default_locale = :ja

文字コードutf8mb4対応(必要であれば)

config/initializers/utf8mb4.rb
module Utf8mb4
  def create_table(table_name, options = {})
    table_options = options.merge(options: 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
    super(table_name, table_options) do |td|
      yield td if block_given?
    end
  end
end

ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters
    class AbstractMysqlAdapter
      prepend Utf8mb4
    end
  end
end

webpackerのインストール

rails 6.0から必須になりました。

$ npm install -g yarn
$ rails webpacker:install

webpackerでjs.erbを使用できるようにする

jsをerbで使用する方がほとんどだと思います。

$ bundle exec rails webpacker:install:erb

サーバーの起動

jsをホットリロードしてくれるので、

$ bin/webpack-dev-server

を実行したあとにサーバー起動するのをおすすめします。

$ rails s -b 0.0.0.0

終わりに

とりあえず以上で動くようにはなります。
あとは適宜設定していただければと思います。
ザーっと書きなぐったかんじなので、不足している点があればコメントいただければ幸いです。

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

【初心者】VScodeの置換機能でカテゴリを書く

某プログラミングスクールでメルカリ作成中の備忘録です。

カテゴリ多過ぎ

パンくずリストを作成しようとしていたところ、肝心の並べるべきカテゴリ作って無いやんけ!となり、
こちら2つの記事を参考に、まずカテゴリ全部入れてしまおうと試みました。

多階層カテゴリでancestryを使ったら便利すぎた
rubyを使ってメルカリのカテゴリのseedファイルを作る

メンバーがすでにancestryを導入して少し作ってくれていたので、seedファイルの方の記事を参考にVScodeの置換機能の練習がてらに並べて見ました。

目標

seed.rb
lady = Category.create(:name=>"レディース")

lady_tops = lady.children.create(:name=>"トップス")
lady_jacket = lady.children.create(:name=>"ジャケット/アウター")

lady_tops.children.create([{:name=>"Tシャツ/カットソー(半袖/袖なし)"}, {:name=>"Tシャツ/カットソー(七分/長袖)"},{:name=>"その他"}])
lady_jacket.children.create([{:name=>"テーラードジャケット"}, {:name=>"ノーカラージャケット"}, {:name=>"Gジャン/デニムジャケット"},{:name=>"その他"}])

このlady_tops.children.createの長すぎる配列をコピペと置換で並べます。

手順

前任者がancestryの記事を参考に、レディースの子要素を入れ終わったあたりで力尽きていたので、とりあえずレディースの孫要素を全部入れます。

検証ツールのconsoleで$('h3').empty()で要素消せるってかなり目から鱗だったのですが、何故かh4だけ消えてくれず………
スクリーンショット 2019-10-23 4.11.30.png

まあ並べる時消せば良いかとそのまま続行、全コピペ
スクリーンショット 2019-10-23 4.33.33.png

これ以降から検索窓に書いてあるように打っていけば大丈夫です

"その他"の下に改行入れます
スクリーンショット 2019-10-23 4.36.29.png

孫カテゴリの頭に"すべて"があるので消します
スクリーンショット 2019-10-23 4.38.10.png

囲みます
2枚繋げてます
スクリーンショット 2019-10-23 5.06.30.png

空のやつを消します
スクリーンショット 2019-10-23 4.41.15.png

","の後にある改行のみを”、”だけに置き換えることで良い感じに並びました
スクリーンショット 2019-10-23 4.42.14.png
h4消せなかったけど逆にわかりやすくて結果オーライ感
あとはくくってあげるだけですね。他のも同じ要領でやればすぐできそうです。

感想

こんな便利機能があるなんて露知らず、手打ちしようとしていました。まるで自転車を漕がず手で押すようなものですね。
仕事でバリバリやってる方々ではやっぱり常識なのでしょうか?もっと効率の良いやり方もありそうですね。
とりあえずの初心者の備忘録でした。

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

Mac+homebrew+Rails でPostgreSQL入門

はじめに

この記事は備忘録として書いています。
悪い点は忌憚なくご指摘いただけると幸いです。

環境

Homebrew 2.1.15
psql (PostgreSQL) 11.5

PostgreSQLのインストールが完了した後

まず以下のコマンドでデータベースの一覧を確認してみる。
$ psql -l

k$ psql -l

                        List of databases
   Name    | Owner | Encoding | Collate | Ctype | Access privileges 
-----------+-------+----------+---------+-------+-------------------
 postgres  | k     | UTF8     | C       | C     | 
 template0 | k     | UTF8     | C       | C     | =c/k             +
           |       |          |         |       | k=CTc/k
 template1 | k     | UTF8     | C       | C     | =c/k             +
           |       |          |         |       | k=CTc/k
(3 rows)

データベースはデフォルトでこのようになっている。

PostgreSQLがデフォルトで参照するデータベースクラスタの設定

vimコマンドで~/.bash_profileというファイルへ
$ vi ~/.bash_profile
以下を追加する
export PGDATA=/usr/local/var/postgres

データベースの作成

psqldbというデータベースをpsqluserというロール名で作成する
$ createdb psqldb -O psqluser

$ psql -l

  List of databases
   Name    |  Owner   | Encoding | Collate | Ctype | Access privileges 
-----------+----------+----------+---------+-------+-------------------
 psqldb    | psqluser | UTF8     | C       | C     | 
 postgres  | k        | UTF8     | C       | C     | 
 template0 | k        | UTF8     | C       | C     | =c/k             +
           |          |          |         |       | k=CTc/k
 template1 | k        | UTF8     | C       | C     | =c/k             +
           |          |          |         |       | k=CTc/k
(3 rows)

psqldbというデーターベースにpsqluserというロールをオーナーとして入る
$ psql -U psqluser psqldb

// psqluser というロール名に postgres というパスワードを設定
psqldb=> alter role psqluser with password 'postgres';
ALTER ROLE

//ロール名の一覧を確認
paqldb=> \du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 k         | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
 psqluser  |                                                            | {}

paqluserにはなんの権限もない状態なので権限を付与する

psqldb=> ALTER ROLE psqluser WITH CREATEDB;
ERROR:  permission denied

むむむ、エラーだ
一度kというユーザーとしてpsqldbに接続

$ psql -U k psqldb


psqldb=# ALTER ROLE psqluser WITH createdb;
ALTER ROLE
psqldb=# ALTER ROLE psqluser WITH createrole;
ALTER ROLE
psqldb=# ALTER ROLE psqluser WITH Superuser;
ALTER ROLE
psqldb=# \du
                                    List of roles
 Role name |                         Attributes                         | Member of  
-----------+------------------------------------------------------------+------------
 k         | Superuser, Create role, Create DB, Replication, Bypass RLS | {psqluser}
 psqluser  | Superuser, Create role, Create DB                          | {}

psqluser に無事権限を付与できた

よくあるエラー

 k$ postgres -D /usr/local/var/postgres
2019-10-22 23:04:38.695 JST [14260] FATAL:  lock file "postmaster.pid" already exists
2019-10-22 23:04:38.695 JST [14260] HINT:  Is another postmaster (PID 13895) running in data directory "/usr/local/var/postgres"?

正しく停止させないとpid情報が残ってしまう
以下を実行
rm /usr/local/var/postgres/postmaster.pid

2019-10-22 23:04:57.924 JST [14265] LOG:  listening on IPv6 address "::1", port 5432
2019-10-22 23:04:57.924 JST [14265] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2019-10-22 23:04:57.926 JST [14265] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2019-10-22 23:04:57.950 JST [14266] LOG:  database system was interrupted; last known up at 2019-10-22 22:49:54 JST
2019-10-22 23:04:58.084 JST [14266] LOG:  database system was not properly shut down; automatic recovery in progress
2019-10-22 23:04:58.087 JST [14266] LOG:  redo starts at 0/167D3D0
2019-10-22 23:04:58.087 JST [14266] LOG:  invalid record length at 0/167D4B0: wanted 24, got 0
2019-10-22 23:04:58.087 JST [14266] LOG:  redo done at 0/167D478
2019-10-22 23:04:58.100 JST [14265] LOG:  database system is ready to accept connections

$ postgres -D /usr/local/var/postgres
もう一度実行
こんなログが出れば完了

参考文献

https://qiita.com/yh2020/items/8be3087004d100fe752b
http://ponsuke-tarou.hatenablog.com/entry/2018/01/31/232012
https://codenote.net/mac/homebrew/3894.html
https://codenote.net/mac/homebrew/187.html
https://teratail.com/questions/112476
https://qiita.com/sibakenY/items/407b721ad1bd0975bd00
http://db-study.com/archives/121

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