20191023のRubyに関する記事は18件です。

AWSを使ってホームページを公開するまで。

 AWSで新規プロジェクトを立ち上げようとしたらよくわからない不具合で止まったのでその全貌のメモ。

1 AWSでアカウントを作る。

 リージョンは東京。
 別の地域、特に現在地から遠いところだと最大0.1秒程度のラグが出るらしい。

2 EC2インスタンス立ち上げ。

  • EC2を起動
  • キーペア作成
  • インスタンスIDをコピーしてメモしとく
  • Elastic IPとインスタンスIDを関連付ける
  • セキュリティグループを開いてポートの開放
    • インバウンドタブを開いて編集
    • ルールの追加
    • HTTPを選択して保存
terminal
$ cd
$ mv Downloads/XXX.pem  .ssh/
$ cd .ssh/
$ chmod 600 XXX.pem
$ ssh -i XXX.pem ec2-user@Elastic IP

 これでSSH接続完了。

  [ヒント]

terminal
-bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such file or directory

 上記のようなエラーが出た場合は、

  1. ターミナルをいったん閉じる。
  2. Terminal -> Preferences -> Profiles -> Advanced
  3. 最下部の Set locale environment variables on startup のチェックを外す。
  4. もう一度SSH接続し直す。

 これで改善される。

3 環境づくり

 まずパッケージのアップデートを行う。

terminal
$ sudo yum update

 次に一度に必要なものをインストール。

terminal
$ sudo yum install \
git make gcc-c++ patch \
libyaml-devel libffi-devel libicu-devel \
zlib-devel readline-devel libxml2-devel libxslt-devel \
ImageMagick ImageMagick-devel \
terminal
$ sudo yum install -y openssl-devel

Node.jsをインストール

terminal
$ sudo curl -sL https://rpm.nodesource.com/setup_6.x | sudo bash -
$ sudo yum install nodejs

rbenvとruby-buildをインストール

terminal
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source .bash_profile
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv rehash
$ rbenv install X.X.X
$ # これはかなり長くかかる場合もあるので痺れを切らせて閉じたり変な入力をしないように!
$ rbenv global X.X.X
$ rbenv rehash
$ ruby -v
$ # 入れたバージョンと同じなら問題なし。

MySQLのインストール

 今回は5.6を入れる。

terminal
$ sudo yum install mysql56-server mysql56-devel mysql56
$ sudo service mysqld start
$ sudo service mysqld status
$ # ここで running と出れば大丈夫。
$ sudo /usr/libexec/mysql56/mysqladmin -u root password 'XXXpasswordxxx'
$ mysql -u root -p
$ # パスワードを入れて問題なければ quit で終了。

SSH鍵ペアを作る

terminal
$ ssh-keygen -t rsa -b 4096
$ cat ~/.ssh/id_rsa.pub
$ # これで出てきた文字列をコピーする。
  • GitHubのhttps://github.com/settings/keysに行く。
  • New SSH key を押してタイトルを入れ、keyのところに先ほどコピーした文字列を入力する。
terminal
$ ssh -T git@github.com

unicorn導入

gemfile
group :production do
  gem 'unicorn', '5.4.1'
end

 config/unicorn.rb を作成。

config/unicorn.rb
app_path = File.expand_path('../../', __FILE__)
worker_processes 1
working_directory app_path
pid "#{app_path}/tmp/pids/unicorn.pid"
listen 3000
stderr_path "#{app_path}/log/unicorn.stderr.log"
stdout_path "#{app_path}/log/unicorn.stdout.log"
timeout 60
preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true
check_client_connection false
run_once = true
before_fork do |server, worker|
 defined?(ActiveRecord::Base) &&
   ActiveRecord::Base.connection.disconnect!
 if run_once
   run_once = false # prevent from firing again
 end
 old_pid = "#{server.config[:pid]}.oldbin"
 if File.exist?(old_pid) && server.pid != old_pid
   begin
     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
     Process.kill(sig, File.read(old_pid).to_i)
   rescue Errno::ENOENT, Errno::ESRCH => e
     logger.error e
   end
 end
end
after_fork do |_server, _worker|
 defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

 と入力する。

terminal
$ sudo mkdir /var/www/
$ sudo chown ec2-user /var/www/
$ cd /var/www/
$ git clone リポジトリURL
$ # このリポジトリURLは各リポジトリを選択した際に右のほうに出る clone or download という緑色のボタンのところに出る。

gemのインストール

terminal
$ cd /var/www/リポジトリ名
$ # 以降はこのディレクションで作業する
$ ruby -v

 ローカルでの開発環境で bundler のバージョンを確認。

local_terminal
$ bundler -v
terminal
$ gem install bundler -v X.X.X
$ # 先ほど確認したバージョンのものを入れる。
$ bundler install
$ # この作業もかなり時間がかかる場合がある。

環境変数の設定

terminal
$ rake secret
$ # これで表示された文字列をコピーしておく。
$ sudo vim /etc/environment
$ # 入力後に i を押してインサートモードにし、

DATABASE_PASSWORD='MySQLのパスワード'
SECRET_KEY_BASE='さっきの文字列'

 入力後は esc を押した後に :wq で保存して終了。

  [ヒント]
 :q!で保存せずに終了。
 出られなくなったりめちゃくちゃになった時はこれ!

 その後 exit でいったんログアウトする。

terminal
$ env | grep SECRET_KEY_BASE
$ env | grep DATABASE_PASSWORD

 これで現在の環境変数を確認できる。
 セキュリティグループを選択してインバウンドタブの編集から、
 カスタムTCP、3000番ポートを開放する設定を追加。

4 データベースを作る

config/database.yml
production:
  <<: *default
  database: XXX
  username: root
  password: <%= ENV['DATABASE_PASSWORD'] %>
  socket: /var/lib/mysql/mysql.sock

 変更を加えた時は必ず

termianl
$ git pull origin master

 をする。

 そして現在(2019/10/23)の問題点……

terminal
$ rails db:create
Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
Couldn't create 'XXX_development' database. Please check your configuration.
rails aborted!
Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
/var/www/XXX/bin/rails:9:in `<top (required)>'
/var/www/XXX/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:create
(See full trace by running task with --trace)

 以前まではこれで普通にデータベースを作れていたのだが、
 何故か今回このような表示が出てデータベースが作れなくなってしまった。
 エラー画面を元に様々な解決法を検索して試してみたが、
 (2)が別の数字に変わるだけで依然進展せず、
 インスタンスをかれこれ3回も初めからやり直すことに……

 解決法が分かり次第更新するが、もしこれを見て解決法が分かりそうな方は是非お力を貸していただきたいです!

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

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で続きを読む

【メモ】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で続きを読む

式展開について備忘録

式展開とは

式展開とは、文字列の中に式や変数を展開すること。
文字列と変数、式の計算結果を連結させる場合に、
それぞれを+で連結させなくても、文字列に変換しなくても簡単に連結できる方法。
また、文字列と数値を結合しようとした場合、式展開は自動的に数値を文字列に変換する。

式展開をつかわない結合
ex)

sample.rb
name = 'test'
puts '私は' + name + 'です。'

#出力結果
私はtestです。

式展開を使った結合

sample.rb
name = 'test'
puts "私は#{name}です"

#出力結果
私はtestです。

文字列と計算結果の結合
ex)

sample.rb
puts '合計金額は' + (100+100*0.08).to_s + '円です。'

#出力結果
合計金額は108.0円です。

式展開を使った場合

sample.rb
puts "合計金額は#{100+100*0.08}円です。"

#出力結果
合計金額は108.0円です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyでじゃんけんアプリを作ってみた(2)

今回はメソッドなしのベタ書きのパターンで書いたので、メソッドを用いてみる。

2019/10/24 追記)
コメントを頂いたので改修しました。
(scivolaさん、ありがとうございます)

仕様要件

・プレイヤーは標準入力で使用する手を選択する
・選択せずにEnterを押下、もしくは想定外の値では次に進まないようにする
・勝負がついたらおしまい、あいこになったら決着がつくまでおこなう
・じゃんけん処理をメソッドを定義する
・スマートにできそうなところは改修する。

実際に書いてみた

janken.rb
# ジャンケンメソッド
def jankens

  # 自分の手(入力処理)
  player = nil
  # 未入力、もしくは指定外の値を入れるとループする
  while player.nil? || player > 3 || player == 0 do
    puts "[1]グー\n[2]チョキ\n[3]パー"
    player = gets.to_i
  end

  # 相手の手(乱数処理)
  enemy = rand 1..3

  # 固定文言
  puts "\n\r相手「ぽん!」\n\r"

  # 勝負
  jankens = [nil, "グー", "チョキ", "パー"]
  puts "プレイヤー:#{jankens[player]}"
  puts "相手   :#{jankens[enemy]}\n\r"

  # 判定
  if player == enemy
    puts "相手「あいこで・・・」\n\r"
    return true
  elsif (player == 1 && enemy == 2) || (player == 2 && enemy == 3) || (player == 3 && enemy == 1)
    puts "You win!!"
    return false
  else
    puts "You lose."
    return false
  end

end

# ゲーム開始文言
puts "相手「最初はグー、じゃんけん・・・」\n\r"

# ループ構文
loop_flg = true
while loop_flg do
  loop_flg = jankens
end

exit

ソースコードで使ったもの

メソッドの定義化

今回くらいの規模だとメソッドの定義をするまでもないけど、規模が大きくなって繰り返すことが多くなれば、メソッドの定義化を使う場面が増えてくる。

サンプルコード
def guiter
  p "Gibson"
end
guiter
出力例
Gibson

今回のソースコードには、returnで返り値でループフラグを返すようにしている。
具体的には、あいこならtrue、決着がついたらfalseを返すようにして、呼び出すメソッドの前後をループさせるようにしている。

今回はやっていないが、引数を使うとこういう風になる。

サンプルコード
def guiter(type)
  p "このギターは、#{guiter}です。"
end
guiter("Lespaul")
guiter("Flying V")
出力例
このギターは、Lespaulです。
このギターは、Flying Vです。

ランダムメソッドと数値の加算

相手の手の処理を一部改修した。

前回のソースコード
  # 相手の手(乱数処理)
  enemy = nil
  # 未入力、もしくは指定外の値を入れるとループする
  while enemy.nil? || enemy > 3 || enemy == 0 do
    enemy = rand(4)
  end
今回のソースコード
  # 相手の手(乱数処理)
  enemy = rand(3) + 1

お分かりだろうか。
前回は、初回のnilの状態でスタートし、0〜3の何れかの数値が出た後、0ならやり直すようにしているのだ。
今回は、ランダムメソッドで0〜2の何れかの数値が出た後に+1して、1〜3の何れかの数値になるようにしている。そのため、ループせず一発で決まるようになった。

2019/10/24 追記)

改修したソースコード
  # 相手の手(乱数処理)
  enemy = rand 1..3

ご指摘いただき記載し直しました。
1〜3の数値のうちランダムで選択するというもの。

確かにこちらだと、処理が一目で分かりますね。

完成して…

今の段階だと、この後にどうすれば、もっとスマートになるのかわからないのでご指摘いただけると幸いです。

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

Rubyの基本箇所についての復習②(#{ })

はじめに

前回の記事でRubyの最も基本的な部分について書かせて頂きました。
今回はもう少し詳しくRubyの基礎について書きます。

内容

変数の応用的な使い方

実行

前回の記事を読んで頂ければ、Rubyを使って文字を出力させる方法がわかって頂けると思います。
例えば'私の趣味は読書です'と出力したい場合は、

puts "私の趣味は読書です"

という風にすればよいのでした。
同じように'私の趣味はスポーツです'と出力したい場合は、

puts "私の趣味はスポーツです"

とすればいいです。

しかし、このコードを見てみると無駄が多いですね。
どちらの文章も'私の趣味は'と'です'の部分は共通していて、
'読書'と'スポーツ'の部分だけ変更すれば良さそうです。

そこで、前回にも登場した変数を使ってコードを書いてみたいと思います。

hoby = "読書"
puts "私の趣味はhobyです"

こうすれば'私の趣味は読書です'と出力されるでしょうか?
残念ながらそうはなりません。
前回の記事の最後で、
puts "word"という風に、変数をダブルクォーテーション(")で囲んでしまうと、そういう文字だと認識され、'word'と出力されてしまうという事を書かせて頂きました。
今回も同じ事が起き、実際に出力されるのは'私の趣味はhobyです'という、狙いとは違った文章が出力されてしまいます。

それでは、どうすれば狙い通り'私の趣味は読書です'と出力できるのでしょうか?

Rubyでこのような事を行いたい場合は、下記のようにコードを書けば良いです。

hoby = "読書"
puts "私の趣味は#{hoby}です"

このようにコードを書けば、狙い通りの'私の趣味は読書です'という文章が出力されます。
さっきと変わった部分は、hobyの部分を#{hoby}と書き換えただけです。
同じように、

hoby = "スポーツ"
puts "私の趣味は#{hoby}です"

とすれば、'私の趣味はスポーツです'という文章が出力されます。

このように#{}を使えば、代入する変数を変えるだけで、違う文章を出力させることができます。
今回のような短い文章ではありがたみが分りにくいですが、もっと長い文章を出力したり、または1つだけではなくもっと多くの文章を出力しなければならなくなった時に、この方法のありがたみが解ってきます。

終わりに

今回の記事の方法だと、変数を一々コード内に書かなければいけないという弱みがあります。
次回の記事では、これを解決する方法について書きたいと思います。

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

Rubyの基本箇所についての復習②

はじめに

前回の記事でRubyの最も基本的な部分について書かせて頂きました。
今回はもう少し詳しくRubyの基礎について書きます。

内容

変数の応用的な使い方

実行

前回の記事を読んで頂ければ、Rubyを使って文字を出力させる方法がわかって頂けると思います。
例えば'私の趣味は読書です'と出力したい場合は、

puts "私の趣味は読書です"

という風にすればよいのでした。
同じように'私の趣味はスポーツです'と出力したい場合は、

puts "私の趣味はスポーツです"

とすればいいです。

しかし、このコードを見てみると無駄が多いですね。
どちらの文章も'私の趣味は'と'です'の部分は共通していて、
'読書'と'スポーツ'の部分だけ変更すれば良さそうです。

そこで、前回にも登場した変数を使ってコードを書いてみたいと思います。

hoby = "読書"
puts "私の趣味はhobyです"

こうすれば'私の趣味は読書です'と出力されるでしょうか?
残念ながらそうはなりません。
前回の記事の最後で、
puts "word"という風に、変数をダブルクォーテーション(")で囲んでしまうと、そういう文字だと認識され、'word'と出力されてしまうという事を書かせて頂きました。
今回も同じ事が起き、実際に出力されるのは'私の趣味はhobyです'という、狙いとは違った文章が出力されてしまいます。

それでは、どうすれば狙い通り'私の趣味は読書です'と出力できるのでしょうか?

Rubyでこのような事を行いたい場合は、下記のようにコードを書けば良いです。

hoby = "読書"
puts "私の趣味は#{hoby}です"

このようにコードを書けば、狙い通りの'私の趣味は読書です'という文章が出力されます。
さっきと変わった部分は、hobyの部分を#{hoby}と書き換えただけです。
同じように、

hoby = "スポーツ"
puts "私の趣味は#{hoby}です"

とすれば、'私の趣味はスポーツです'という文章が出力されます。

このように#{}を使えば、代入する変数を変えるだけで、違う文章を出力させることができます。
今回のような短い文章ではありがたみが分りにくいですが、もっと長い文章を出力したり、または1つだけではなくもっと多くの文章を出力しなければならなくなった時に、この方法のありがたみが解ってきます。

終わりに

今回の記事の方法だと、変数を一々コード内に書かなければいけないという弱みがあります。
次回の記事では、これを解決する方法について書きたいと思います。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Ruby 入門#2(変数、定数)

Rubyの勉強アウトプット
今回は、変数、定数について記載します。

変数

命名ルール

●先頭文字を大文字にすることはできない。
●1文字目に数字を使用することはできない。
●宣言と初期化を同時に行うこと(変数の宣言のみはNG)

変数種別

●変数の1文字目により、変数種別が決まります。
●種別ごとに、スコープの範囲が違います。

ローカル変数

●1文字目が、小文字または'_'で始まる変数は、ローカル変数になります。
●スコープは、定義された場所の範囲内のみ利用可能です。

name = "田中"
puts name

グローバル変数

●1文字目に、「$」をつけるとグローバル変数となります。
●スコープは、メソッドやクラスを超えて参照することができ、定義した場所に限らず、参照や変更をすることができます。

$name = "田中"

class NameClass
  def nameShow
    puts $name
  end
end

nameClass = NameClass.new
nameClass.nameShow

インスタンス変数

●1文字目に、「@」をつけるとインスタンス変数となります。
●スコープは、宣言されたクラスまたはサブクラスから参照することができます。
●オブジェクト毎に、異なる値を割り当てることができます。

class ClassName

  def setName(setName)
    @name = setName
  end

  def showName()
    puts @name
  end

end

name = ClassName.new
name.setName("田中")

name2 = ClassName.new
name2.setName("佐藤")

name.showName  #=>田中
name2.showName #=>佐藤

クラス変数

●変数の先頭に、「@@」をつけるとクラス変数となります。
●スコープは、宣言されたクラス、インスタンスメソッドから参照、変更をすることができます。
●宣言されたクラスの、全てのインスタンスで共有されます。

class ClassName

  def setName(setName)
    @name = setName
  end

  def showName()
    puts @name
  end

end

name = ClassName.new
name.setName("田中")

name2 = ClassName.new
name2.setName("佐藤")

name.showName  #=>佐藤
name2.showName #=>佐藤

定数

●先頭文字をアルファベット大文字にすると定数となります。
●Rubyでは、定数定義後に再代入を行うと、警告が発生しますが値は変更されます。

Name = "aaa"
Name = "bbb"
puts Name
#出力
warning: already initialized constant Name
warning: previous definition of Name was here
佐藤
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Rubyで配列を展開したい

hashの配列を返すメソッドを用意したとして

def foo_method
  ['a', 'b'].map.with_index do |str, index|
    {str.to_sym => index}
  end
end

こんな形で値を取得しようとすると配列の中に配列ができてしまう

[
  {hoge: 1},
  {fuga: 2},
  foo_method
]
=> [{:hoge=>1}, {:fuga=>2}, [{:a=>0}, {:b=>1}]]

でもほんとは配列の中にハッシュが並んでるだけのものが欲しい
flattenとかしてみたけど何も変わらないw

Rubyの配列展開
こちらのページを参考にしたおかげで解決しました
メソッドに"*"を付けてあげます

[
  {hoge: 1},
  {fuga: 2},
  *foo_method
]
=> [{:hoge=>1}, {:fuga=>2}, {:a=>0}, {:b=>1}]

foo_method内でreturnするときに*を付けたりしたけど、それではダメ

def foo_method
  ret = ['a', 'b'].map.with_index do |str, index|
    {str.to_sym => index}
  end
  *ret
end

配列の中で展開するからこそできる技であって、自分を分解して返すものではないみたいです

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

Rubyの扱い方

RubyでFile(ファイル)を書き込む方法【初心者向け】
https://techacademy.jp/magazine/7800

メソッドの定義と呼び出し
https://www.javadrive.jp/ruby/method/index2.html

【Ruby入門ガイド】アプリ開発で知っておくべき知識を総ざらい
https://www.sejuku.net/blog/curriculums-ruby#i-19

Ruby ではじめるプログラミング 【第 1 回】:このサイトなかなかよき?‍♂️
https://magazine.rubyist.net/articles/0002/0002-FirstProgramming.html

20分ではじめるRuby
https://www.ruby-lang.org/ja/documentation/quickstart/2/

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

AWS Cloud9 に Ruby 開発環境をセットアップする

AWS Cloud9 で Rubyのプログラム開発をする際の開発環境をセットアップしてみました。

結論

初めから Ruby2.6 がインストールされているので、特に何もしなくても Rubyによるプログラミングが始められそうです。

環境

  • AWS Cloud9

前提条件

  • AWS アカウント登録済み
  • AWS Cloud9 サービス利用中

AWS Cloud9 に Ruby の開発環境をセットアップする

作業用ディレクトリの作成

実行中の Cloud9 環境に作業用のディレクトリを作成します。

  • 左側ツリーのトップディレクトリを右クリックして [New Folder] を選択
  • フォルダ名を入力します

Ruby のバージョンを確認

ターミナルを開き、現在の環境のRubyのバージョンを確認します。

$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

今回はこのままで進めます。

gem のリストを確認

現在の環境にインストール済みのgems一覧を確認します。
思ったより沢山インストール済みです。

$ gem list --local

*** LOCAL GEMS ***

actioncable (5.0.0)
actionmailer (5.0.0)
actionpack (5.0.0)
actionview (5.0.0)
activejob (5.0.0)
activemodel (5.0.0)
activerecord (5.0.0)
activesupport (5.0.0)
arel (7.1.4)
bigdecimal (default: 1.4.1)
builder (3.2.3)
bundler (default: 1.17.3)
bundler-unload (1.0.2)
cmath (default: 1.0.0)
concurrent-ruby (1.1.5)
crass (1.0.4)
csv (default: 3.0.9)
date (default: 2.0.0)
did_you_mean (1.3.0)
e2mmap (default: 0.1.0)
erubis (2.7.0)
etc (default: 1.0.1)
executable-hooks (1.6.0)
fcntl (default: 1.0.0)
fiddle (default: 1.0.0)
fileutils (default: 1.1.0)
forwardable (default: 1.2.0)
gem-wrappers (1.4.0)
globalid (0.4.2)
i18n (0.9.5)
io-console (default: 0.4.7)
ipaddr (default: 1.2.2)
irb (default: 1.0.0)
json (default: 2.1.0)
logger (default: 1.3.0)
loofah (2.3.0)
mail (2.7.1)
matrix (default: 0.1.0)
method_source (0.9.2)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
mutex_m (default: 0.1.0)
net-telnet (0.2.0)
nio4r (1.2.1)
nokogiri (1.10.4)
openssl (default: 2.1.2)
ostruct (default: 0.1.0)
power_assert (1.1.3)
prime (default: 0.1.0)
psych (default: 3.1.0)
rack (2.0.7)
rack-test (0.6.3)
rails (5.0.0)
rails-dom-testing (2.0.3)
rails-html-sanitizer (1.3.0)
railties (5.0.0)
rake (12.3.2)
rdoc (default: 6.1.0)
rexml (default: 3.1.9)
rss (default: 0.2.7)
rubygems-bundler (1.4.5)
rvm (1.11.3.9)
scanf (default: 1.0.0)
sdbm (default: 1.0.0)
shell (default: 0.7)
sprockets (4.0.0)
sprockets-rails (3.2.1)
stringio (default: 0.0.2)
strscan (default: 1.0.0)
sync (default: 0.5.0)
test-unit (3.2.9)
thor (0.20.3)
thread_safe (0.3.6)
thwait (default: 0.1.0)
tracer (default: 0.1.0)
tzinfo (1.2.5)
webrick (default: 1.4.2)
websocket-driver (0.6.5)
websocket-extensions (0.1.4)
xmlrpc (0.3.0)
zlib (default: 1.0.0)

bunder を更新

bundler (default: 1.17.3) を更新します。
最新は (2.0.2)

$ gem update bundler
Updating installed gems
Updating bundler
Fetching bundler-2.0.2.gem
Successfully installed bundler-2.0.2
Parsing documentation for bundler-2.0.2
Installing ri documentation for bundler-2.0.2
Installing darkfish documentation for bundler-2.0.2
Done installing documentation for bundler after 5 seconds
Parsing documentation for bundler-2.0.2
Done installing documentation for bundler after 2 seconds
Gems updated: bundler

bundler で ライブラリをインストール

bundle init

$ bundle init
Writing new Gemfile to /home/ec2-user/environment/ruby_test/Gemfile

Gemfile に インストールするライブラリを追加

gem "dotenv"

bundle install

$ bundle install
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using bundler 2.0.2
Fetching dotenv 2.7.5
Installing dotenv 2.7.5
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
$ gem list --local

*** LOCAL GEMS ***

actioncable (5.0.0)
actionmailer (5.0.0)
actionpack (5.0.0)
actionview (5.0.0)
activejob (5.0.0)
activemodel (5.0.0)
activerecord (5.0.0)
activesupport (5.0.0)
arel (7.1.4)
bigdecimal (default: 1.4.1)
builder (3.2.3)
bundler (2.0.2, default: 1.17.3)
bundler-unload (1.0.2)
cmath (default: 1.0.0)
concurrent-ruby (1.1.5)
crass (1.0.4)
csv (default: 3.0.9)
date (default: 2.0.0)
did_you_mean (1.3.0)
dotenv (2.7.5)
e2mmap (default: 0.1.0)
erubis (2.7.0)

dotenv がインストールされました。

テスト

ライブラリが読み込めて利用できることをテストします。

hello.rb
require 'rubygems'
require 'bundler/setup'
require 'dotenv/load'

def hello(name)
    puts "#{ENV['HELLO']} #{name}!!"
end

hello("ruby")

.envファイル

HELLO=Hello

実行結果

$ ruby -cw hello.rb 
Syntax OK
$ ruby -w hello.rb 
Hello ruby!!

kintone のアプリの情報の取得

フィールドの一覧を取得する
https://developer.cybozu.io/hc/ja/articles/204783170#anchor_getform_fields

拙稿からコードをコピーして実行してみます。
https://qiita.com/sy250f/items/82f84904daf7601eeb08#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%81%AE%E5%8F%96%E5%BE%97

アプリID=100のアプリのフィールド一覧を取得します。

実行結果

$ ruby -cw get_app_fields.rb 
Syntax OK
$ ruby -w get_app_fields.rb 
{"revision"=>"24",
 "properties"=>
  {"備考"=>
    {"type"=>"MULTI_LINE_TEXT",
     "code"=>"備考",
     "label"=>"備考",
     "noLabel"=>false,
     "required"=>false,
     "defaultValue"=>""},
   "レコード番号"=>
    {"type"=>"RECORD_NUMBER",
     "code"=>"レコード番号",
     "label"=>"レコード番号",
     "noLabel"=>false},
   "作業者"=>
    {"type"=>"STATUS_ASSIGNEE",
     "code"=>"作業者",
     "label"=>"作業者",
     "enabled"=>true},
   "割引率"=>
    {"type"=>"NUMBER",
     "code"=>"割引率",
     "label"=>"割引率",
     "noLabel"=>false,
     "required"=>false,
     "minValue"=>"",
     "maxValue"=>"",
     "digit"=>false,
     "unique"=>false,
     "defaultValue"=>"",
     "displayScale"=>"",
     "unit"=>"",
     "unitPosition"=>"BEFORE"},
   "更新者"=>
    {"type"=>"MODIFIER", "code"=>"更新者", "label"=>"更新者", "noLabel"=>false},
   "作成者"=>{"type"=>"CREATOR", "code"=>"作成者", "label"=>"作成者", "noLabel"=>false},
   "ステータス"=>
    {"type"=>"STATUS", "code"=>"ステータス", "label"=>"ステータス", "enabled"=>true},
   "更新日時"=>
    {"type"=>"UPDATED_TIME",
     "code"=>"更新日時",
     "label"=>"更新日時",
     "noLabel"=>false},
   "金額"=>
    {"type"=>"NUMBER",
     "code"=>"金額",
     "label"=>"金額",
     "noLabel"=>false,
     "required"=>false,
     "minValue"=>"",
     "maxValue"=>"",
     "digit"=>true,
     "unique"=>false,
     "defaultValue"=>"",
     "displayScale"=>"",
     "unit"=>"",
     "unitPosition"=>"BEFORE"},
   "カテゴリー"=>
    {"type"=>"CATEGORY", "code"=>"カテゴリー", "label"=>"カテゴリー", "enabled"=>false},
   "file"=>
    {"type"=>"FILE",
     "code"=>"file",
     "label"=>"file",
     "noLabel"=>false,
     "required"=>false,
     "thumbnailSize"=>"150"},
   "商品名"=>
    {"type"=>"SINGLE_LINE_TEXT",
     "code"=>"商品名",
     "label"=>"商品名",
     "noLabel"=>false,
     "required"=>false,
     "minLength"=>"",
     "maxLength"=>"64",
     "expression"=>"",
     "hideExpression"=>false,
     "unique"=>true,
     "defaultValue"=>""},
   "備考_0"=>
    {"type"=>"MULTI_LINE_TEXT",
     "code"=>"備考_0",
     "label"=>"備考",
     "noLabel"=>false,
     "required"=>false,
     "defaultValue"=>""},
   "備考_4"=>
    {"type"=>"MULTI_LINE_TEXT",
     "code"=>"備考_4",
     "label"=>"備考",
     "noLabel"=>false,
     "required"=>false,
     "defaultValue"=>""},
   "備考_3"=>
    {"type"=>"MULTI_LINE_TEXT",
     "code"=>"備考_3",
     "label"=>"備考",
     "noLabel"=>false,
     "required"=>false,
     "defaultValue"=>""},
   "作成日時"=>
    {"type"=>"CREATED_TIME",
     "code"=>"作成日時",
     "label"=>"作成日時",
     "noLabel"=>false},
   "備考_2"=>
    {"type"=>"MULTI_LINE_TEXT",
     "code"=>"備考_2",
     "label"=>"備考",
     "noLabel"=>false,
     "required"=>false,
     "defaultValue"=>""},
   "備考_1"=>
    {"type"=>"MULTI_LINE_TEXT",
     "code"=>"備考_1",
     "label"=>"備考",
     "noLabel"=>false,
     "required"=>false,
     "defaultValue"=>""}}}

rubyプログラムから kintoneアプリのフィールド一覧を取得できました。

関連リンク

Ruby関連

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

本日の学び #6

Array#max, Array#min

配列の中で、最大、最小の要素を返す。

str[idx]

p "hello"[-2] #=> "l"

Stirng#end_with?(*strs)

self の末尾が strs のいずれかであるとき true を返す。
引数は複数指定可能

Strign#chop

文字列の最後の文字を取り除いた新しい文字列を生成して返す。

お世話になりました

[Ruby]特定の文字列の抽出
Array#max
Array#min
String#end_with?
String#chop

感想

paizaの簡単なB問題なら解けるようになってきた!
1日空いてしまったが、明日も頑張ろう:muscle:

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