- 投稿日:2019-10-23T23:24:50+09:00
FactoryBotで高速に大量データを作る方法
FactoryBotで大量のデータを作りたいときどうしていますか?
FactoryBotにはcreate_listという簡単に大量データが作れる便利なメソッドがあります。
ただ、とても便利なcreate_listですがパフォーマンス観点で見ると問題があります。
この記事ではサンプルを使って挙動を確認していきます。実行環境
Ruby: 2.6.5
Rails: 6.0.0
rspec-rails: 3.9
factory_bot_rails: 5.1.1Models
id, name, created_at, updated_atのカラムを持ったUserモデルを使います。
db/migrate/20190914140349_create_users.rbclass CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name t.timestamps end end endRSpec
use create_list
ユーザー一覧を取得するリクエストスペックです。
100件のユーザーをbeforeで作成しています。spec/requests/users_spec.rbrequire '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.logUser 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.rbcontext '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.logUser 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.rbcontext '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.logUser 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
- 投稿日:2019-10-23T23:01:17+09:00
エラー時の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=httpsHeroku logの読み方
ログはこのような形式で出力される。
timestamp source[dyno]: messagetimestamp:ログの出力時刻
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はPostgreSQLHerokuで動かすためには本番環境のDBをSQLiteからPostgreSQLに変えないといけない。
そのために3つの手順が必要↓こちらを参考に
https://qiita.com/hmmrjn/items/e2dff8036fbbd74f049a
- 投稿日:2019-10-23T22:53:16+09:00
【メモ】Rails APIモード × MySQL5.7 × Docker で環境構築
はじめに
久しぶりにDockerを作成したら所々躓いたので要点をメモ
手順
- ディレクトリ用意&移動
Gemfile,Gemfile.lock,Dockerfile,docker-compose.ymlを作成- 2.のファイルにコード記述
$ docker-compose build$ docker-compose run app rails new --api . --force --no-deps --database=mysqlconfig/database.ymlを修正$ docker-compose build --no-cache$ docker-compose up$ docker-compose run app rails db:create手順3, 6で用意するファイルについて
Gemfilesource 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } source "https://rubygems.org" gem 'rails', '6.0.0'DockerfileFROM 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 . /appdocker-compose.ymlversion: '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: localconfig/database.ymldefault: &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'] %>
- 投稿日:2019-10-23T21:38:41+09:00
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 # ... endauthentication_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).firstresource_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/) endget_case_insensitive_field_from_resource_params(field)は、resource_params[login]にスペースがあったら削除してくれるメソッドです。:emailキーの場合は、downcase!してくれますが、今回は:loginキーにしているので関係ないです。
あとはfind_resourceメソッドで入力された情報に合うユーザーがいるか探しにいきましょう。(クエリの発行)
ヒットしたら@resoucerにそのユーザーを返します。
その後の処理は、もともとの処理と変更ありませんので、解説はしません。
これで、「メールアドレス」または「電話番号」での認証について、成功です。
参考
- 投稿日:2019-10-23T20:08:06+09:00
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個/週です。各週の実績
8/12(月)・8/19(月)の週に進捗がないのは、夏季休暇を取る人が多かったからです。私自身、8/12(月)の週は夏季休暇取りました。
最初は勢いがあったのですが、後半は僕ともう一人誰かくらいしか書いてくれなかったです。(忙しそうだったのでお願いもしきれませんでした。)個人の実績
累積値を表示してます。日付のセルに▲のマークが隅に付いてますが、上期評価の際に振り返るために、何があったか簡単にメモを書いてました。最後の平均値ですが、6/3(月)の週と8/5(月)の週を除いた16週で平均を取ってます。
私は言い出しっぺなので当然目標
ほぼ達成です。(達成じゃないんかい!)進捗管理表
これまで乗せてきたグラフや表はこの進捗管理表から自動生成されてます。
pathsファイルというのがrailsで言うところのコントローラーに対応してます。
後で紹介しますが、r2-oasは「コントローラー毎にAPIドキュメントが書ける」特徴があります。レビュー体制
レビューは私が一人で行いました。複数人でレビューしている時間なんてありませんでした。
そんなに厳しく見るのではなく以下の点に注意して見ました。レビュー修正してもらってる時間はなかったので私がやってました。
- SwaggerUIで開いてエラーにならないか
- SwaggerEditorで開いてエラーにならないか
- タイポしてないか
- レスポンスのschemaやexampleが適切か
- 考えうるクエリパラメーターはちゃんと書いてあるか
- HTTPステータスは間違えてないか
- 一部未完成状態ならsummaryに
[TODO]が付いているか.pathsファイルの更新(後から説明します。)です。最初に雛形を一気に作成するので、ドキュメントの
構造がしっかりしているかをレビューする必要がなかったです。進捗報告
こんな感じで毎週月曜日にslackで進捗を報告し、「今週も頑張らなきゃって」思わせるようにしてました。
ワンポイントアドバイスとかも書いてました。メンバーが「今週こそは頑張らねば」って思ったかどうかは知りません。(笑)まとめ
チームメンバー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:latestrails 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こんな感じの画面が開くと思います。
こんな感じでファイルが自動生成されている事が確認できるかと思います。
このようにファイル分割する事で、
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 endcomponents/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側の問題にしました。
$refの再帰的読み込みの際にstack too deepが起こるケースがあり得た。
kubernetesのcomponents/schemasにはJSONSchemaが使ってあることが分かりました。こんな感じで書いてあって見事に無限ループに陥りました。
JSONSchemaPropsがJSONSchemaPropsを参照してます。現在では修正済みです。
"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があるというエラーなどが出ました。yamlファイルで管理しているので適宜修正すればいいだけなので特に何もしませんでした。
型変換までサポートするとツールが複雑になるのでサポートしませんでした。leaddeskの場合
pathパラメーターの定義がないとSwaggerEditorでエラーになる。
SwaggerEditorで開くとpathパラメターがないよとvalidationエラーが起きました。これも頑張ればツール側で吸収しようと思えば出来ましたが、結構大変だったのでやめました。leaddesk側の問題にしました。
V3でサポートしてない型でエラー
V2ではサポートしていたがV3でサポートされてない
typeがあるというエラーなどが出ました。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で報告しようと思ってます。お楽しみに!
- 投稿日:2019-10-23T20:06:41+09:00
(個人メモ) Rails5モデルの関連付けるとき 気をつけましょう
前提
Railsでモデルを更新するようなコードを書こうとして、思いの外ハマって変更しない関連させていたモデルもロードさせる
テーブル構造
以下簡単なテーブルを作ってみます。
とりあえず書いてみる
ひとまず自分の思うがままにコードを書いていく。
# 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以上
- 投稿日:2019-10-23T20:01:10+09:00
ActiveJobとSidekiqを組み合わせたときに、ジョブ単位のオプションはどこまで設定できるか?
背景
非同期処理
一般的なブラウザは、送信したHTTPリクエストが30秒以内に応答されない時はタイムアウトし、そのHTTPリクエストは失敗したと判定します。Webアプリケーションが長時間かかる処理を行いたい場合は、ブラウザがタイムアウトしないように、HTTPリクエストに応答を一旦返し、その後実際の処理を実行します。このような処理をリクエストの応答と実処理の完了が同期していない(同時でない)点から、非同期処理と呼びます。
非同期処理フレームワーク
非同期処理では、一つ一つの処理をジョブと呼びます。Webアプリケーションは、HTTPリクエストを受け取ると、次のように動作します。
- リクエストに応じたジョブを作成
- ジョブをキューと呼ばれる領域に保存
- 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を読んでください。
参考
たとえばRubyGems.orgからは6000万回ダウンロードされています。 ↩
retry_onはRails 5.1.0で追加された機能です。それ以前のバージョンのActiveJobを使う場合は自分で実装する必要があります。ActiveJobでリトライ制御 - Qiitaなどを参考にしてください。 ↩
ActiveJob::TestHelperの使用例が知りたい方はRSpec でキューイングした ActiveJob を同期実行する - QiitaやDelayed Job Queue Adapter in RSpec with Rails 5.1 - Today I Learnedを読んでください。 ↩
- 投稿日:2019-10-23T19:47:14+09:00
【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つが必要なのかは、よく分かっていないので
教えていただけら嬉しいです。
- 投稿日:2019-10-23T11:52:49+09:00
Rails6 のちょい足しな新機能を試す98(add_autoload_paths_to_load_path編)
はじめに
Rails 6 に追加された新機能を試す第98段。 今回は、
add_autoload_paths_to_load_path編です。
Rails 6 では、zeitwerkmode で不要なパスを $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_sandboxconfig/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.rbputs $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_pathをfalseに変更することが推奨されています。試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try098_add_autoload_paths_to_load_path参考情報
- 投稿日:2019-10-23T09:12:51+09:00
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に仮想マシンを作成
- 仮想マシンの作成
- 名前: rails_test(任意)
- マシンフォルダー: (任意)
- タイプ: Linux
- バージョン: Red Hat (64-bit)
以降は任意で設定。今回はデフォルトのままで進めました。
仮想マシンが作成されたら、「設定」を押下します。
「システム」→「マザーボード」→「起動順序」のハードディスクの順序を一番上に持ってきてください。
これをしないと、OSインストール直後の再起動時にisoファイルが再読み込みされます。「ネットワーク」→「アダプター 2」で「ネットワークアダプターを有効化」にチェックを入れ、割り当てを「ホストオンリーアダプター」に設定。
OSインストール
赤色の枠を付けたところを設定します。
- 時刻と日付...アジア/東京
- ソフトウェアの選択...最小限のインストール
- インストール先...デフォルト
- ネットワークとホスト名...Ethernet(enp0s3), Ethernet(enp0s8)の接続を両方ONにする。
リリースノートによると、Virtual Boxを使用した上で「ソフトウェアの選択」がデフォルトのままだと問題があるようです。
ここまでできたら、インストールの開始をします。rootのパスワードを設定します。ユーザーは後で作成するのでここでは何もしません。
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 makeRuby・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 mysqldFirewallの設定
ポートの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 installproject/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: <<: *defaultI18n言語設定(必要であれば)
・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.rbmodule 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 endwebpackerのインストール
rails 6.0から必須になりました。
$ npm install -g yarn $ rails webpacker:installwebpackerでjs.erbを使用できるようにする
jsをerbで使用する方がほとんどだと思います。
$ bundle exec rails webpacker:install:erbサーバーの起動
jsをホットリロードしてくれるので、
$ bin/webpack-dev-serverを実行したあとにサーバー起動するのをおすすめします。
$ rails s -b 0.0.0.0終わりに
とりあえず以上で動くようにはなります。
あとは適宜設定していただければと思います。
ザーっと書きなぐったかんじなので、不足している点があればコメントいただければ幸いです。
- 投稿日:2019-10-23T05:40:25+09:00
【初心者】VScodeの置換機能でカテゴリを書く
某プログラミングスクールでメルカリ作成中の備忘録です。
カテゴリ多過ぎ
パンくずリストを作成しようとしていたところ、肝心の並べるべきカテゴリ作って無いやんけ!となり、
こちら2つの記事を参考に、まずカテゴリ全部入れてしまおうと試みました。多階層カテゴリでancestryを使ったら便利すぎた
rubyを使ってメルカリのカテゴリのseedファイルを作るメンバーがすでにancestryを導入して少し作ってくれていたので、seedファイルの方の記事を参考にVScodeの置換機能の練習がてらに並べて見ました。
目標
seed.rblady = 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だけ消えてくれず………
これ以降から検索窓に書いてあるように打っていけば大丈夫です
","の後にある改行のみを”、”だけに置き換えることで良い感じに並びました
h4消せなかったけど逆にわかりやすくて結果オーライ感
あとはくくってあげるだけですね。他のも同じ要領でやればすぐできそうです。感想
こんな便利機能があるなんて露知らず、手打ちしようとしていました。まるで自転車を漕がず手で押すようなものですね。
仕事でバリバリやってる方々ではやっぱり常識なのでしょうか?もっと効率の良いやり方もありそうですね。
とりあえずの初心者の備忘録でした。
- 投稿日:2019-10-23T02:45:55+09:00
Mac+homebrew+Rails でPostgreSQL入門
はじめに
この記事は備忘録として書いています。
悪い点は忌憚なくご指摘いただけると幸いです。環境
Homebrew 2.1.15
psql (PostgreSQL) 11.5PostgreSQLのインストールが完了した後
まず以下のコマンドでデータベースの一覧を確認してみる。
$ psql -lk$ 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.pid2019-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

























