20191223のRubyに関する記事は14件です。

【Rails 6】(初心者向け)Ajax版最小構成CRUDアプリ(ページ移動をゼロに!)

Railsの学習で最初に学ぶのが,メッセージの作成・表示(読み取り)・更新・削除のできるCRUDアプリであると思います。私も最初にCRUDアプリを作成したときは,正直理解が追いつきませんでした:sweat_smile:

ところが,実際に作成してみるといろいろと不満が出てくることでしょう。一番は「見た目」だと思いますが,

「ボタンをクリックするたびにページを移動するのは嫌だなあ……」

と思いませんでしたか?特に本番環境では読み込みに時間がかかってしまいます。そこで,この記事では,ページ遷移ゼロの最小構成CRUDアプリを作成していきたいと思います:grinning:

通常のCRUDアプリを理解していたならば,本記事も理解できるようなるべく丁寧に解説していきます。

この記事では,Rails標準の Ajax の使い方を学ぶことだけに焦点を当てます。そのため,見た目・バリデーション・例外処理など細かいことは全て削ります。なお, jQuery の使用は避けて, Javascript を使用することとします。

完成後のアプリ

ajax_crud_sample.gif

開発環境

  • macOS Catalina 10.15.2
  • Ruby 2.6.4
  • Rails 6.0.2

手順

0. 準備

  • まずはアプリを作成し,最低限の準備をしていきます。
    • Heroku へのデプロイまで挑戦されたい場合は, rails new のところで -d postgresql オプションを付け, $ rails db:create も実行して下さい。
ターミナル
$ rails new ajax_crud_sample
$ cd ajax_crud_sample
$ rails g controller messages index
$ rails g model Message content:string
$ rails db:migrate
  • 念のため,$ rails sでサーバーを起動し,http://localhost:3000にアクセスし,「Yay! You’re on Rails!」を確認しておいて下さい。

  • CRUDアプリを作成するための,ルーティングを設定しておきます。

config/routes.rb
Rails.application.routes.draw do
-  get 'messages/index'
+  root 'messages#index'
+  resources :messages
end
  • 一覧ページに投稿メッセージが表示されるようにします。
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end
end
app/views/messages/index.html.erb
<!-- 各メッセージに対し,部分テンプレート _message.html.erb を呼び出す -->
<%= render @messages %>
app/views/messages/_message.html.erb
<p>
  <%= message.content %>
</p>

  • 確認用として,適当なメッセージをデータベースに投入します
db/seeds.rb
messages = %w[おはよう こんにちは こんばんは]
messages.each do |message|
  Message.create!(content: message)
end
puts '初期データの保存に成功しました!'
ターミナル
$ rails db:seed
  • トップページ(http://localhost:3000)にアクセスし,次のように表示されたならばOKです!

スクリーンショット 2019-12-23 10.54.26.png

1.メッセージの作成

準備ができましたので,まずはメッセージを投稿し,ページ遷移無しでそのメッセージを追加表示できるようにしましょう!

  • 最初にフォームを作成します。
app/views/messages/index.html.erb
<!-- 部分テンプレート _form.html.erb を呼び出す -->
<%= render 'form' %>
<hr>
<!-- 各メッセージに対し, _message.html.erb を呼び出す -->
<%= render @messages %>
app/views/messages/_form.html.erb
<!-- 新規メッセージ投稿用のフォーム -->
<%= form_with model: Message.new do |f| %>
  <%= f.text_field :content %>
  <%= f.submit "投稿" %>
<% end %>
  • ここでトップページを確認してみて下さい。次のように表示されていればOKです!

スクリーンショット 2019-12-23 11.12.59.png

もちろん,このままでは「投稿ボタン」を押しても,何も起こりません。対応するcreateアクションでメッセージの保存を指示します。

app/controllers/messages_controller.rb
  # (略)
  # ********** 以下を追加 ********** 
  def create
    Message.create!(message_params)
  end

  private

  # Strong Parameters はサボらずに使っておくこととします
  def message_params
    params.require(:message).permit(:content)
  end
  # ********** 以上を追加 ********** 
end

この時点で,フォームに入力して投稿してみて下さい。ページ内には変化は起きませんが,SQLが発行されていることをターミナルから確認できます。ページを更新してみて下さい。先ほど投稿したメッセージが反映されているはずです!

ここで,通常のCRUDアプリならば,例えば次のように書くでしょう。

app/controllers/messages_controller.rb
  def create
    Message.create!(message_params)
    # 実際に次の一行を追加して確認してもよいですが,その後削除して下さい!
    redirect_to root_path
  end

このようにすれば,新規メッセージを投稿 するだけでなく ページ更新 まで行われますので,一応,新規投稿機能ができたことになります。

ところが,今回は ページ遷移無し で投稿メッセージを追加表示させたいので,これではダメです:scream:ここで,トップページのソースを確認してみます。

<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">
<!-- (略) -->
</form>

フォームの箇所に, data-remote="true" が入っているはずです。

form_withでフォームを作成した場合は,local: trueオプションを付けない限り,自動的にこのデータ属性が追加されます。

この data-remote="true"含まれていない 場合は,コントローラの createメソッドで指示をしない限り, create.html.erb が呼び出されます。create.html.erb を自動で呼び出すにはlocal: trueオプションを付けなければなりません。
(なお, form_forform_tag の使用は 現在推奨されておりません

では, data-remote="true"含まれている 場合はどうなるのでしょうか。実は,自動的に Ajax を利用して非同期通信(ページ遷移無しに通信)が行われ, create メソッドが実行されます。更に,指示がない場合は create.js.erb が呼び出されるようになるのです。つまり,ページ遷移は起こらず,この create.js.erb に書いた処理が実行されるようになるのです!

Javascript を使うことで,ページの一部分を変更することができます。そこで,

  • 投稿したメッセージを,メッセージ一覧の一番下に追加する

というプログラムを書くことで, ページ遷移無し に新規メッセージを追加表示できるのです!具体的には次のように書きます。

app/views/messages/index.html.erb
<%= render 'form' %>
<hr>
<!-- メッセージ一覧を div タグで囲み,idを付けておく -->
<div id="messages">
  <%= render @messages %>
</div>
app/controllers/messages_controller.rb
  # 以下を変更(新規メッセージを create.js.erb で使えるようにインスタンス変数に入れておく)
  def create
    @message = Message.create!(message_params)
  end
  • 次の create.js.erb を作成します。
app/views/messages/create.js.erb
// メッセージ一覧の一番下に新規メッセージを追加する
document.getElementById('messages').insertAdjacentHTML('beforeend', '<%= j(render @message) %>')

Javascriptに慣れていない方もいらっしゃると思いますので,解説を入れます。 document.getElementById('messages') は, messagesというidが付いている要素を取得する操作です。今回のケースならば,次が取得されます。

document.getElementById('messages')
<div id="messages">
    <p>
        おはよう
    </p>
    <p>
        こんにちは
    </p>
    <p>
        こんばんは
    </p>
</div>

ここの</div>の前に新規メッセージを追加したいので,続けて次を書くことになります。

.insertAdjacentHTML('beforeend', //新規メッセージ// )

「新規メッセージ」の箇所は,例えば'<%= @message.content %>' としてもメッセージが追加されるのですが,これではダメです!

'<%= @message.content %>'ただの文字列 です。<p>タグで囲まれていませんので,2回投稿すると前のメッセージと繋がってしまいます

そもそも,単に文字列を追加したいのではなく, _message.html.erb に書いたテンプレートで新規メッセージを追加したいわけです。そこで,j(render @message)と書くことになります。

j とはなんぞや?」と思われたかもしれません。これは, escape_javascript メソッドです。 _message.html.erb 内の改行 \n をエスケープしないとJavascriptの構文エラーが発生します。

さて,メッセージを投稿すれば,ただデータベースに保存されるだけでなく,新規メッセージが一番下に追加表示されるようになりましたが,もう一つ問題があります。投稿したのに, フォームの文字が残ったまま になっています!

ページを更新していないので,指示をしなければ当然フォームの文字も残ったままになります。そこで,create.js.erb に次を追加して下さい。

app/views/messages/create.js.erb
// (略)
// フォームの文字を空にする
document.getElementById('message_content').value = ''

実は,フォームのテキストフィールドに id="message_content" が自動で付いています。これを取得し,値を空にする指示を出せばOKです。ここの id は ハイフン ではなく アンダーバー ですのでご注意下さい。

2. メッセージの削除

次にメッセージの削除機能を付けます:open_mouth:

  • まずは削除用のリンクを作成します。
app/views/messages/_message.html.erb
<!-- idを追加 -->
<p id="message-<%= message.id %>">
  <%= message.content %>
  <!-- 削除のリンクを追加 -->
  <%= link_to '削除', message_path(message),
              method: :delete,
              data: { confirm: '削除しますか?', remote: true } %>
</p>

  • コントローラに次を追加します
app/controllers/messages_controller.rb
  # idからメッセージを取り出す操作は他でも必要となるので最初からまとめておきます
  before_action :set_message, only: %i[destroy]
  # (略)
  def destroy
    @message.destroy!
  end

  private
  # (略)
  def set_message
    @message = Message.find(params[:id])
  end

普通のCRUDアプリとの違いは,まず, data 属性に remote: true を入れていることです。これで,コントローラの destroy メソッドの後にページ遷移が起こらず, destroy.html.erb ではなく destroy.js.erb が動作するようになります。

また,削除するメッセージを特定できるようにするため,各メッセージに id を付けておきます。

この時点でデータベースからメッセージの削除はできます。ところが,何も指示しなければページを更新しない限り,ページ内からメッセージが消えません。そこで,destroy.js.erb にメッセージを削除するプログラムを書きます。

app/views/messages/destroy.js.erb
document.getElementById('message-<%= @message.id %>').outerHTML = ''

ここも解説を入れておきます。削除する @message.id2 であるとすると,

document.getElementById('message-<%= @message.id %>')

により,次が取り出されます。

<p id="message-2">
    こんにちは
    <a data-confirm="削除しますか?" data-remote="true" rel="nofollow" data-method="delete" href="/messages/2">削除</a>
</p>

全てを消去したいので, outerHTML を空にするように指示します。これでページ遷移無しにメッセージが消えるようになりました!

3. メッセージの更新

メッセージの更新は少々大変です。普通のCRUDアプリならば,更新ボタンを押した後「更新用のページ」に移動させます。今回はページ遷移無しに更新部分を実装したいので,まずは更新ボタンを押した時に「更新用のフォーム」を表示できるようにしなければなりません。

なるべく簡単にしたいと思いますので,この記事では「新規メッセージの投稿フォーム」を「更新用のフォーム」に置き換えることにします。

  • まずは,更新フォームを呼び出すリンクを作成します。
app/views/messages/_message.html.erb
<p id="message-<%= message.id %>">
  <%= message.content %>
  <!-- ********** 以下を追加 ********** -->
  <%= link_to '更新', edit_message_path(message), data: { remote: true } %>
  <!-- ********** 以上を追加 ********** -->
  <%= link_to '削除', message_path(message),
              method: :delete,
              data: { confirm: '削除しますか?', remote: true } %>
</p>

  • フォームを更新用としても使えるように,フォームの部分テンプレートを修正しておきます。
app/views/messages/_form.html.erb
<!-- Message.new を 変数 message に変更 -->
<%= form_with model: message do |f| %>
  <%= f.text_field :content %>
<!-- ボタンの文字を 変数 value に変更 -->
  <%= f.submit value %>
<% end %>
app/views/messages/index.html.erb
<!-- 新規投稿メッセージなので,ボタンの文字は「投稿」とする -->
<%= render 'form', message: Message.new, value: '投稿' %>
<hr>
<div id="messages">
  <%= render @messages %>
</div>

この時点で次のような表示になります。

スクリーンショット 2019-12-23 19.57.35.png

  • コントローラに edit, update を追加します。
app/controllers/messages_controller.rb
  # edit, update でも呼び出すようにする
  before_action :message_set, only: %i[destroy edit update]

  # 更新用のフォームに置き換えることだけに使用する
  def edit
  end

  def update
    @message.update!(message_params)
  end

更新のリンクをクリックしたときに動作するのは edit.js.erb です。ここに,「更新用のフォーム」に置き換える操作を書きましょう。

app/views/messages/edit.js.erb
// フォームは一つしか無いので, id を使わず,タグ名から要素を取得
// ここの変数宣言に let や const を使用すると2回目にエラーが発生します
var form = document.getElementsByTagName('form')[0]
// フォームを更新用フォームに置き換えます。ボタンの文字は「更新」にします。
form.outerHTML = '<%= j(render 'form', message: @message, value: '更新') %>'

例えば,2番目のメッセージにある更新ボタンをクリックすると,次のような表示に変わります。

スクリーンショット 2019-12-23 21.01.06.png

コントローラにデータベースを更新する操作はすでに入れています。あとは更新用フォームの「更新」ボタンを押したときに,ページ内のメッセージを更新するようにします。

app/views/messages/update.js.erb
// 対応するメッセージを更新
document.getElementById('message-<%= @message.id %>').innerHTML = '<%= j(render @message) %>'
// フォームを新規投稿フォームに戻す
var form = document.getElementsByTagName('form')[0]
form.outerHTML = '<%= j(render 'form', message: Message.new, value: '投稿') %>'

これで,メッセージの更新も ページ遷移無し で実現できるようになりました:grin:

4. おまけ

最後に,フォームを「更新用」に変更したあと,「新規投稿用」に戻すリンクを作っておきます。見た目がいまいちですが,本筋から外れますので許して下さい:sweat_smile:

  • 更新用フォームにだけ「取消」のリンクを追加
app/views/messages/_form.html.erb
<%= form_with model: message do |f| %>
  <%= f.text_field :content %>
  <%= f.submit value %>
<!-- ********** 以下を追加 ********** -->
  <% if value == '更新' %>
    <%= link_to '取消', new_message_path, data: { remote: true } %>
  <% end %>
<!-- ********** 以上を追加 ********** -->
<% end %>
  • コントローラに追加
app/controllers/messages_controller.rb
  # 新規投稿用のフォームに置き換えることだけに使用する
  def new
  end
  • 「取消」ボタンを押したとき,「新規投稿用フォーム」に戻すようにする
app/views/messages/new.js.erb
var form = document.getElementsByTagName('form')[0]
form.outerHTML = '<%= j(render 'form', message: Message.new, value: '投稿') %>'

これで,「新規投稿用フォーム」に戻すこともできるようになりました。

今回の記事は Ajax を利用して ページ遷移無し でCRUDアプリを作成することのみに重点をおいて解説をしました。少しでも理解を進めるお手伝いができたならば幸いです。お疲れ様でした:grinning:

最終的なコード

$ git clone https://github.com/T-Tsujii/ajax_crud_sample.git
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.orderで並び替えたデータ取得時の注意点

映画レビューアプリを作成中に気づいたことです!

レビュー投稿画面に移行時に
①Movieテーブル最新データ1つ取得→ fleshmovie = Movie.order(updated_at: :desc).limit(1)
②最新データidカラムの値をインスタンス変数に定義→ @movie = fleshmovie.id

reviews_controller.rb
def new
  fleshmovie = Movie.order(updated_at: :desc).limit(1)
  @movie = fleshmovie.id
end
error_code
[2] pry(#<ReviewsController>)> @movie
  Movie Load (0.6ms)  SELECT  `movies`.* FROM `movies` ORDER BY `movies`.`updated_at` DESC LIMIT 1
  ↳ app/controllers/reviews_controller.rb:10
=> [#<Movie:0x00007fb5d5e845d0
  id: 37,
  mtdb_id: "10315",
  created_at: Mon, 23 Dec 2019 11:08:37 UTC +00:00,
  updated_at: Mon, 23 Dec 2019 11:08:37 UTC +00:00>]
[3] pry(#<ReviewsController>)> @movie.id
NoMethodError: undefined method `id' for #<Movie::ActiveRecord_Relation:0x00007fb5d6b6bd98>
Did you mean?  ids

@movieで最新データは取得できているのに、idカラムが参照できない!

rails documentを確認
.orderは並び替えるメソッドなので、@movieは配列を取得している気がする
まず配列の1つめを取得する必要がありそう!

fleshmovie.id → fleshmovie[0].idで解決しました!

reviews_controller.rb
def new
  fleshmovie = Movie.order(updated_at: :desc).limit(1)
  @movie = fleshmovie[0].id
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CircleCiで、mysqlのDBに接続してテストを通す。ruby on rails

ruby on railsのCircleCiで結構はまったのでメモ

データベースの接続設定

appli/config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

test:
  <<: *default
  username: 'root'
  password: 'rootpass'
  port: 3306
  host: '127.0.0.1'
  database: ci_test
circleci/config.yml
# Ruby CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
#
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here 
      - image: circleci/ruby:2.6.5-node-browsers
        environment:
          BUNDLER_VERSION: 2.0.1
          RAILS_ENV: test
          DB_HOST: 127.0.0.1
          DB_USERNAME: 'root'
          DB_PASSWORD: 'rootpass'

      - image: circleci/mysql:5.7.22
        environment:
          MYSQL_DATABASE: 'ci_test'
          MYSQL_USER: 'root'
          MYSQL_ROOT_PASSWORD: 'rootpass'
      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/repo

    steps:
      - checkout
      - run:
          name: yarn install
          command: yarn install
      - run:
          name: setup bundler
          command: |
            sudo gem update --system
            sudo gem uninstall bundler
            sudo rm /usr/local/bin/bundle
            sudo rm /usr/local/bin/bundler
            sudo gem install bundler
        # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            yarn install --check-files
            bundle install --jobs=4 --retry=3 --path vendor/bundle

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # Database setup
      - run:
          name: bundle exec
          command: |
            dockerize -wait tcp://127.0.0.1:3306 -timeout 120s
            bundle exec rake db:create
            bundle exec rake db:schema:load

      # run tests!
      - run:
          name: run tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"

            sudo gem install bundler
            sudo gem install rspec
            sudo gem install rspec-core
            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      # collect reports
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

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

【環境構築】Docker + Rails6 + Vue.js + Vuetifyの環境構築手順

はじめに

Docker + Rails6 + Vue.js + Vuetifyの開発環境構築手順をまとめました。

以下の記事を参考にさせて頂きました!ありがとうございます:bow_tone1:

環境

OS: macOS Catalina 10.15.1
zsh: 5.7.1
Ruby: 2.6.5
Rails: 6.0.2
Docker: 19.03.5
docker-compose: 1.24.1
Vue: 2.6.10
vue-router: 2.6.10
vuex: 3.1.2
vuetify: 2.1.0

1.準備

作成するアプリケーション名はhogeappとします。
まずは以下ファイルを作成して下さい。

Dockerfile

yarnが必要になるので、Dockerfileに反映しています。

hogeapp/Dockerfile
FROM ruby:2.6.5

RUN apt-get update -qq && \
  apt-get install -y build-essential \
  libpq-dev \
  nodejs \
  vim

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
  curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
  echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
  apt-get update && apt-get install -y yarn

RUN mkdir /app_name
ENV APP_ROOT /app_name
WORKDIR $APP_ROOT

COPY ./Gemfile $APP_ROOT/Gemfile
COPY ./Gemfile.lock $APP_ROOT/Gemfile.lock
RUN bundle install

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT [ "entrypoint.sh" ]
ADD . $APP_ROOT

docker-compose.yml

hogeapp/docker-compose.yml
version: "3"
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    volumes:
      - db-data:/var/lib/mysql
    ports:
      - "3306:3306"
  web:
    build: .
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    links:
      - db
    tty: true
    stdin_open: true
    depends_on:
      - db
  data:
    image: busybox
    volumes:
      - db-data:/var/lib/mysql
    tty: true
volumes:
  db-data:
    driver: local

Gemfile

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

gem 'rails', '~> 6.0.2', '>= 6.0.2.1'

Gemfile.lock

中身は空で作成します。

hogeapp/Gemfile.lock

entrypoint.sh

hogeapp/entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /app_name/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

これで必要なファイルが揃ったので、次はRailsアプリの作成です。

2.Railsアプリの作成

rails new

$ docker-compose run web rails new . --force --database=mysql --skip-bundle

※このエラーが出たら

Error response from daemon: OCI runtime create failed: container_linux.go:346: starting container process caused "exec: \"rails\": executable file not found in $PATH": unknown

$ docker-compose build

※何らかのgemが不足しているようなエラーが出たら

自分は以下のようなエラーが発生しました。

Could not find public_suffix-4.0.1 in any of the sources

$ docker-compose run web bundle install

一度bundle installを試してみて下さい。

database.ymlの変更

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password #ここを変更
  host: db #ここを変更

コンテナが立ち上がるか確認

docker-compose up -dだとログが見えないので、無事に環境構築が完了するまでは-dなしが良いと思います。

$ docker-compose up

db:create

無事にコンテナが立ち上がったら、DBを作成しましょう。

$ docker-compose exec web rails db:create

Yay! You’re on Rails!

localhost:3000にアクセスして確認してみましょう。

image.png

これでRailsはOKなので、次はVue.jsです!

3.Vue.jsの導入

webpackerのインストール

$ docker-compose exec web rails webpacker:install

Vue.jsのインストール

$ docker-compose exec web rails webpacker:install:vue

Vue.jsとの連携を確認

Railsでコントローラーを作ってみて、Vue.jsと連携出来るかを確認してみます。

$ docker-compose exec web rails g controller home index
app/views/home/index.html.erb
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

このように変更し、hello_vue.jsを読み込みます。

※Vue.jsのインストール時にhello_vue.jsはデフォルトで作成されています。

Railsのルーティングを設定

config/routes.rb
root to: 'home#index'

ブラウザで確認

localhost:3000にアクセスし、下記画面が出力されているか確認してみて下さい。

image.png

これでVue.jsの導入はOKですが、他の単一ファイルコンポーネントも作成し、読み込めるかどうかを確認しておきます。

4.他の単一ファイルコンポーネントが読み込めるかどうか確認する

Top.vueの作成

app/javascript/components/ディレクトリを作成し、その中にTop.vueを作成します。

Top.vueの中身は以下のようにしました。

app/javascript/components/Top.vue
<template>
  <section id="top">
    <h1>This is Top.vue!</h1>
  </section>
</template>

<script>
  export default {
    name: 'Top'
  }
</script>

<style>
  h1 {
    text-align: center;
  }
</style>

app.vueを変更

app/javascript/app.vue
<template>
  <div id="app">
    <p>{{ message }}</p>
    <Top/> //追記
  </div>
</template>

<script>
import Top from "./components/Top"; //追記

export default {
  data: function () {
    return {
      message: "Hello Vue!"
    }
  },
  components: {
    Top, //追記
  }
}
</script>
...以下略

ブラウザで確認

再度読み込みすると、以下のような画面になっているはずです。

image.png

これできちんと単一ファイルコンポーネントが読み込まれていることが確認できたので、Vue.jsはOKです。

次はラスト!Vuetifyの導入です。

5.Vuetifyの導入

Vuetifyのインストール

$ docker-compose exec web yarn add vuetify -D

hello_vue.jsに追記

hello_vue.js
import Vue from 'vue'
import Vuetify from 'vuetify' //追加
import "vuetify/dist/vuetify.min.css" //追加
import App from '../app.vue'

Vue.use(Vuetify) //追加

const vuetify = new Vuetify(); //追加

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    vuetify, //追加
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

  console.log(app)
})

application.html.erbの変更

application.html.erbでVuetify用にフォントとアイコンの読み込みと不要な箇所の削除を行います。

app/views/layouts/application.html.erb
  <head>
...略
    <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
...略
  </head>

下記タグは不要なので削除しておきます。

app/views/layouts/application.html.erb
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

これで準備が整ったので、あとはVuetifyのコンポーネントが反映されるかどうか、作成して確認してみましょう。

Header.vueの作成

新規にHeader.vueを作成し、<v-app-bar>を組み込んでみます。

app/javascript/components/Header.vue
<template>
  <header id="header">
    <v-app-bar>
      <v-app-bar-nav-icon></v-app-bar-nav-icon>
      <v-toolbar-title>This is Header.vue</v-toolbar-title>
    </v-app-bar>
  </header>
</template>

<script>
  export default {
    name: 'Header',
  }
</script>

app.vueを変更

Header.vueを読み込みつつ、全体をまとめている<div><v-app>タグに変更しておきます。

app/javascript/app.vue
<template>
  <v-app id="app"> //divから変更
    <Header/> //追記
    <h1>This is app.vue</h1> //一応追記。どっちでもいいです
    <p>{{ message }}</p>
    <Top/>
  </v-app>
</template>

<script>
import Header from "./components/Header"; //追記
import Top from "./components/Top";

export default {
  data: function () {
    return {
      message: "Hello Vue!"
    }
  },
  components: {
    Header, //追記
    Top,
  }
}
</script>

これで再度localhost:3000にアクセスし、問題ないかを確認してみます。

image.png

無事に画像のようなヘッダーが表示されていれば完了です!

以上です!お疲れ様でした!:clap:

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかのお役に立てれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

欅坂46の歌詞を形態素解析してみた(MeCabをRubyで使う)

はじめに

12月14日に平成Ruby会議01に参加してきました。

階層的クラスタリングをRubyで表現する」のセッションに影響されて、
普段、自然言語処理や統計などやっていない素人がやってみました。

環境

macOS Cataline 10.15.2
Ruby 2.5.3
Homebrew 2.2.1

インストール

MecabとMecabで使用する辞書をインストールします。

$ brew install mecab
$ brew install mecab-ipadic
$ mecab -v                      
mecab of 0.996

続いて、
MecabをRubyで扱うことのできるnattoをインストールします。

$ gem install natto
$ gem list natto
*** LOCAL GEMS ***

natto (1.1.2)

実装

今回歌詞を取得するため
歌ネット から nokogiri を使ってスクレイピングします。
今回nokogiri に関する説明は省略します。

欅坂46の歌詞の人気順のTOP10に絞って行いました。
https://www.uta-net.com/artist/19868/4/

まず人気一覧から各曲の歌詞ページの飛んで歌詞を取得し、
run_nattoに渡します。

require 'nokogiri'
require 'open-uri'

def run
  doc = Nokogiri::HTML.parse(open('https://www.uta-net.com/artist/19868/4/'), nil, 'utf-8')
  words = []
  doc.css('.td1').each_with_index do |f, i|
    link = f.children[0][:href]
    url = "https://www.uta-net.com#{link}"
    song = Nokogiri::HTML.parse(open(url), nil, 'utf-8')
    title = song.css('.title > h2').text
    run_natto(song, title, words)
    break if i == 9
  end
end

Natto::MeCab.newにノードフォーマットオプションを渡すことで
抽出する属性をカスタマイズすることができます。

今回は
「助詞」、「助動詞」、「記号」は除き、
形態素: ノードの品詞
の形で抽出しています。

人: 名詞
溢れ: 動詞
交差点: 名詞
どこ: 名詞
行く: 動詞
require 'natto'

def run_natto(song, title, words)
  natto = Natto::MeCab.new('-F%m:\s%f[0]')
  natto.enum_parse(song.css('#kashi_area').text).each do |n|
    array = n.feature.split(': ')
    if array[1] == '助詞' || array[1] == '助動詞' || array[1] == '記号'
      next
    else
      words << array[0]
    end
  end
end

run_nattoで生成した配列から
形態素をkeyとして個数をvalueとするハッシュを生成し
CSVで出力しました。

require 'csv'

def run
  ...
  hash = words.group_by(&:itself).map{ |key, value| [key, value.count] }.to_h
  create_csv(hash)
end

def create_csv(hash)
  CSV.open('file.csv', 'w') do |csv|
    csv << %w[ワード カウント]
    hash.each do |key, value|
      csv << [key, value]
    end
  end
end

結果

TOP10は以下のようになりました。

ワード カウント
( 68
50
50
45
39
37
ない 35
31
29
29

「僕」「君」は多いだろうなと思っていたので予想通りでしたが
「()」や「ん」「ない」などあまり参考にならないものが上位に来てしまい
改良の余地があるなと思いました…

ちなみに

ワード カウント
セゾン 24
Blah 24

二人セゾンとアンビバレントですね
(セゾンこんな言ってるんだ…)

最後に

実際の発表ではさらに階層的クラスタリングを行っているのですが、
とりあえずは形態素解析まで行ってみました。

今回は秋元康でしたが別の作詞家でやってみると面白いかと思います!

参考

https://github.com/buruzaemon/natto
https://speakerdeck.com/ayumitamai97/implement-hierarchical-clustering-analysis-using-ruby
https://www.uta-net.com/
http://brainvalley.jp/blog/32
http://brainvalley.jp/blog/33

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

ライブラリの定期アップデートのPRをどこまでレビューするか

前提

あくまで私がどこまで見てるか、です。会社・チーム・文化によって違うと思うので参考程度に見てください。
今まで Ruby の開発がメインだったのでその経験をもとにしています。

思い出しながら書いてるので、えいっと書き出したものなので、書き漏れがあるかもしれないのですが、それはご了承ください。
また、表現が適切じゃない箇所もあるかもしれないのですが、そこもいい感じに読み取っていただけると助かります ><

しない話

  • 定期アップデートをなぜやるのか
  • 定期アップデートをどうやるのか

レビュー時にみるポイント

  • どれくらいバージョンが上がっているのか
    • メジャーなのかマイナーなのかパッチなのか
    • 現在のバージョンはいくつか
  • production 用か devleopment 用か test 用か
  • 言語系か小さなツールか
  • 作者・チームは信頼できるか・きちんとメンテされているか
  • changelogはきちんと書かれてるか
    • どういう変更か・breaking changesがあるか
    • feature/bugfix/breaking changes/docs etc
  • リリースされてからどれくらい経っているか
  • PR/commit/issue/diffをみる
  • テストでカバーできてる範囲の変更か
  • 仮にバグっていたときどこに影響が出るのか

どれくらいバージョンが上がっているのか

https://semver.org/lang/

  • APIの変更に互換性のない場合はメジャーバージョンを、
  • 後方互換性があり機能性を追加した場合はマイナーバージョンを、
  • 後方互換性を伴うバグ修正をした場合はパッチバージョンを上げます。

多くのアプリはこの semantic versioning を採用しているはずなので、
メジャー・マイナー・パッチのどれがどれくらい上がったか、をみるば影響範囲が概ね想定できます。

※ ただ、アプリによっては、 semantic versioning っぽいバージョンの付け方をしていても、 semantic versioning に則ってないこともあるので注意です。

また、0.y.zのようなメジャーバージョンが 0 系のアプリに関しては、破壊的変更が入りやすいので注意深く見ます。

production 用か devleopment 用か test 用か

https://bundler.io/v2.0/man/gemfile.5.html#GROUPS
Ruby の Gem (ライブラリ) 管理ツールである bundler では、
以下のように、gem をどの環境で使うのかを設定できます。

gem 'rails'

group :development, :test do
  gem 'byebug'
end

group :production do
  gem 'pg'
end

production で使う gem であれば、もちろん慎重にいきますが、
developmenttest でつかう gem であれば、多少雑にレビューをしてバグったとしてもバグってから直せば良い、という判断をすることもあります。

ただ、developmenttest でも、『通っちゃいけないテストが通るようになって本番にバグが紛れこんでしまう』ということも否定はできないので、
development test でも十分にレビューが必要なケースはもちろんあります。

言語系か小さなツールか

たとえば rails のような影響範囲・依存度が高い gem はとても慎重にレビュー・テストを実行した上でのマージをするケースが多いですが、
おそらくテストのダミーでターでしか使われないような faker であれば、テストが通ってれば十分だろう、という判断してさくっとマージしてしまうことが多いです。

作者・チームは信頼できるか・きちんとメンテされているか

railsrack などのような、大きなコミュニティによってメンテされている gem であれば、changelog をざっと眺めて BugFix しかないのであればさくっとマージしてしまいます。
逆に、あまりきちんとメンテされてなさそうなそうでない gem の場合、きちんと changelog だけでなく commit/diff を丁寧に確認します。

※ もちろん、OSSとして公開してもらえていて、それを利用しているのでありがたやという気持ちでいっぱいです。

基本的には、外部のものは自分たちの作業の範囲外なので信用しない・しすぎない、という体でいるのが正しいと思っています。
なので、基本丁寧にレビューして、信用できる gem であれば、少し手を抜いても良いかもという立ち位置です。

changelogはきちんと書かれてるか

https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md#530-alpha39-november-5-2019
のように、 Breaking Changes, Features, BugFix のようにグループ分けされて書かれてるととても安心感が高くなります。
きちんと書かれてる場合、たとえば Breaking Changesとあればそこは重点的に見ないといけないでしょうし、BugFix だけであればざっとchangelogを眺めて気になるのがなければ changelog だけ眺めてマージしています。

※ 自分のアプリがバグを踏んでいないか・バグ前提になってないなどはもちろんきちんと確認する必要はあります。

でも、changelog が書かれてない・あまりきちんとメンテされてないケースもたまにあるので、そのような場合は、changelog だけを参考にはせずに commit 履歴や diff を確認したほうが良いでしょう。

また、Featuresなどであれば、自身のチームのアプリに導入できるものがないかを見て、導入できそうなものがあれば導入をします。
rubocopのようなlint系のツールの場合、新しいルールをどうするかをチームで検討するなどもします。

PR/commit/issue/diffをみる

changelog だけでなく、基本的には PR/commit/issue/diff もみます。
全部見てると時間が足りなくなってしまうので絞ったほうが良いと思いますが、
Breaking changes, BugFix, Feature などは、どういう経緯・意図なのか、影響範囲はどれくらいか、などを把握するために見ます。
また、他人のコード・PR・issueをみることで学べることはたくさんあるので、そういう観点でも時間があったり興味があるのは見たりします。

(『見てもわからない >< 』、っていう方ほど理解するために頑張ってみるのをおすすめします。)

人・チーム・文化によっては、diff全部見るべき、という意見の方もいると思うのですが、
その意見は十分に理解できますが、現実的には時間が足りないので、うまい落とし所を見つけていく必要があるなと思っています。

リリースされてからどれくらい経っているか

定期的にupdateしている場合、新しいバージョンがリリースされてから1日もたってない gem のアップデートということもたくさんあります。
OSSとの向き合い方として、そういうのを積極的に取り入れてバグを発見したらバグ報告したり修正のPRをあげたりコントリビュートしたほうが好ましいと思っています。
ですが、現実的にはそうはいかないケースもあります。
内容次第では、1週間ぐらいアップデートをスキップして1週間後にバグ報告などが増えてなければ gem をアップデートするという選択もたまにします。

テストでカバーできてる範囲の変更か

普段のレビューの観点と同様です。

例えば、 faker gem であればテストが通っていれば十分だと思います。
でも、たとえば pumawhenever のような、単体テストではテストしづらい gem もあると思います。
それらの場合、実際動かしてみたりして挙動を確かめます。

仮にバグっていたときどこに影響が出るのか

普段のレビューの観点と同様です。

これをイメージするには、gem のことを知っていて、自身のアプリの全体像を把握してる必要があります。
わからない場合は、チームメンバーに相談します。

バグが起きたときにどう検知・対応できるか、どのタイミングで起こり得そうか、みたいなことをイメージし、
「問題が起きたら直す!」って責任が持てそうであればエイヤッとマージしてしまうこともあります。
マージ後に挙動確認して、ダメだったら即revertします。

とくに、とりあえずマージしてデプロイが無事完了してアプリが起動すればOKだろう、っていうものの場合はとりあえずマージという判断をすることそこそこあります。
(動かしてみないとわからないなら動かしてみればいいじゃん、です。)

CI通らない・アプリが動かなくなったときにどうするか

  • Changelog, それに関連する PR/issue/commit/diff を読む
  • 同様の問題報告などがないか、 issue/PR を探す
  • issue/PR を上げる
  • アップデートを見送るのか・パッチをあてるのか

Changelog, それに関連する PR/issue/commit/diff を読む

まずはきちんとどういうアップデートが行われたのか確認します。
Breaking changesBugfixにある内容を踏んでるケースが多いので、注意深く確認します。

同様の問題報告などがないか、 issue/PR を探す

自身のアプリが、その gem にモンキーパッチをあてたり・特殊な使い方をしてない限り、
同様の問題は他の人の環境でも起きてる可能性が高いです。
なので、すでに別の方が issue/PR を上げてくれている可能性があるので、それを探します。

issue/PR を上げる

だれも issue/PR を上げてないのであれば、issue/PRを上げます。

アップデートを見送るのか・パッチをあてるのか

たとえば、バグを発見してすでに報告済みで数日待てば修正されたバージョンが上る見込みがあるのであれば、
それを待つという選択を取ることは多いです。

しかし、アクティブにメンテされてる gem ばかりではないので、すぐに直る見込みがないケースも十分にあります。
その場合は、アプリにモンキーパッチをあてたり、gem をフォークして修正してそのバージョンを使うなどもあります。

どちらにするかは、状況によるのでチームメンバーと相談しながら決めていきます。

最後に

慣れるまでは時間がかかるしハードルが高いと思います。
ですが、根気よく続けていけば、技術力も上がるはずですし、
良い意味で手を抜いてレビューできる部分がわかってきてそこまで負荷なくレビュー・マージできるようになると思います。

あくまで、私自身の経験としてライブラリアップデートのPRをレビューしてる経験から書いてみました。
会社・チーム・文化によって重視する点が変わると思うので、あくまで一つのやり方として参考にしてもらえればと思います。

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

[Ruby] より細かくMetricsを取得したい [Heroku]

2016年に[Ruby] かしゆかの誕生日!という潔すぎる Advent Calendar を書いてから 3年。またこの地に降り立ちました、しょっさんです。もちろん、本日もかしゆかの誕生日です、おめでとうございます!!!

今日はRuby Advent Calendar 2019の 23日目、今年もかしゆかの誕生日です!! 昨日はfursichさんのネタ待ち、明日はooooooo_qさんです!!

Heroku Ruby Language Metrics(Public Beta) がでたぞ!

public beta なので、大きな声で推奨はできませんが、NewRecli や Scoutを入れるほどでも、標準よりももう少し詳しく Heroku がアプリの Metrics とってくれないかな...。

とれるようになりそうです

それが、この Ruby Language Metrics(Public Beta) です。設定すればよいだけではないのと、動いたり動かなかったりなのは「β」だからということで、事情を知りうまく行かなかったときは、heroku-metrics-feedback@salesforce.com へフィードバックを入れていただけると捗ると思います、どうぞよろしく。(英語でね)

何ができるようになったの?

次の3つの項目が、追加で取れるようになりました。

  • Puma Pool Usage … Puma のキューイング状況が見られます。80%超え始めたら Dyno 追加しようかなみたいな。
  • Free Memory Slots … 単位時間あたりのメモリスロットの状況を可視化します。
  • Heap Objects Count … 割当と開放されたヒープ状況を可視化します。メモリちゃんと開放されてる?

どうやって使うの?

制約条件として

使い方は Rails なら次のでいけます。

  1. Metric を Enableする
  2. 'heroku/metrics' buildpack を「頭に」追加する
  3. "barnes" Gem をインスコ

1. Metric を Enableする

Heroku Dashboard から、Heroku アプリの Metricsへ行って、「歯車」から "Enhanced Language Metrics" to ”Ruby Language metrics" を ON にしてあげます。

1535572120-node-metrics.gif

コマンドなら。

$ heroku labs:enable "runtime-heroku-metrics" -a "my-app-name"
$ heroku labs:enable "ruby-language-metrics" -a "my-app-name"

2. 'heroku/metrics' buildpack を「頭に」追加する

同じく Dashboard の Settings から、"Add buildpack" で 「heroku/metrics」を追加します。一番目になるように、順序を入れ替えてあげます。

1509042807-Screen-Shot-2017-10-26-at-11.32.05-AM.png

コマンドなら。

$ heroku buildpacks:add -i 1 heroku/metrics

3. "barnes" Gem をインスコ

Gemfilebarnes を追加して、bundle installして、再deployしましょう。

gem "barnes"

※ Railsじゃないとき

例えば puma.rb とかに次のものが必要です。

require 'barnes'

before_fork do
  # worker specific setup

  Barnes.start # Must have enabled worker mode for this to block to be called
end

※ workerない場合は動かないので、最低限一つのworkerが必要みたいだけど。

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

Ruby on Rails 初歩の初歩

備忘録

こんにちは!HLDの井出です。(https://t.co/FiYK5Qty3J?amp=1)
今回は、Railsについての備忘録を残していきます。
インストールからscaffoldまでです。

環境

  • Windows10
  • viniciusfs/centos7
  • rails 5.1.3

準備

Rubyのインストール
参考:https://github.com/rbenv/rbenv
流れは、まず管理ツールのrbenvをインストール。
          ↓
インストールできるバージョンの確認
$ rbenv install --list
欲しいバージョンをインストールしたら、
$ rbenv grobal バージョン
$ rbenv rehash
そして、
$ ruby version
で、バージョンが表示されたらOK!!

railsのインストール

$ gem install rails -v x.x.x (Xは欲しいバージョン)

その前に
下拵えで、nodejsとsqlite3をインストールしておきます。しておかないまま、railsのインストールを進めるとjavascriptが分らんけど?って怒られます。。。

起動!!

インストールができたら、任意のディレクトリで、
rails new app
そうすると、勝手にいろいろと作ってくれる。

アドレス確認

$ ip a
でアドレスを確認して、ブラウザに表示してみます。
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:c1:f7:7a brd ff:ff:ff:ff:ff:ff
inet 192.168.33.10/24 brd 192.168.33.255 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fec1:f77a/64 scope link
valid_lft forever preferred_lft forever

192.168.33.10なので、
$ rails server -b 192.168.33.10 -d
として、サーバー立ち上げます。
Runと出たらOK!!
アドレスをコピペしてブラウザに表示させてみます。

scaffoldで簡単なアプリを作ってみる

無事に、鯖が立ち上がったら、sccaffoldで簡単なアプリを作ってみます。
rails g scaffold Memo title:string body:text
とすると、メモアプリをサクッとrailsが用意してくれます。
できたら、
$ rails db:migrate
とします。
ブラウザで192.168.33.10/memosとして表示されたらOK!!

殺してみる

$ ps aux | grep puma
でプロセス番号がわかります。
vagrant のあとに表示されている数字です。
$ kill -9 プロセス番号
とすると、今立ち上げていたアプリを終わらせることができます。

後記

サクッとでしたが、railsのインストールからアプリの立ち上げまでの流れを見てみました。
あとは、コントローラーとか色々作り込んでみてください。
参考になれば嬉しいです。

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

多目的遺伝的アルゴリズムで恋愛シミュレーションゲームでのひとりぼっちを回避する

アマガミの季節になりましたね。
この時期になると、ときめきなメモリアルを作りたいなぁと考える方がいるかも知れません。

そこで、今回は恋愛シミュレーションゲームを人工知能(AI)の力でヒロインを攻略して、ひとりぼっちを回避したいと思います。

……ただ、ヒロインを1人攻略するだけでは面白くありません。複数のヒロインを同時に攻略していきたいと思います。

ソースコードはGithubで公開しておりますので、詳細はそちらをご確認ください。
https://github.com/Adacchi3/MultiObjectiveGA

多目的遺伝的アルゴリズム

今回は多目的遺伝的アルゴリズムを使用します。

多目的遺伝的アルゴリズムは、遺伝的アルゴリズムを多目的最適化に適用したものであり、近似解を探索するメタヒューリスティックアルゴリズムです。

基本的なプロセスは遺伝的アルゴリズムと同様で以下の通りです。

  1. 初期集団の生成
  2. 遺伝子の評価
  3. 交叉
  4. 突然変異
  5. 終了条件になるまで2.-4.を繰り返す

多目的遺伝的アルゴリズムの特徴は、遺伝子の評価に複数の評価関数を使うことです。
複数の評価関数を用いることにより、しばしばトレードオフの関係が生じる場合があります。
ここでは、遺伝子同士を比較し、支配関係である遺伝子を淘汰することによって、パレートの近似解を求めていきます。

恋愛シミュレーションゲーム

今回シミュレーションするゲームはリアルのゲーム……ではなく、多目的遺伝的アルゴリズムに適用できるシミュレーションを作成しました。

プレイヤーは、文系、理系、芸術、スポーツ、休みの5つの行動のいずれかを1日で1回行動することができます。行動したものに応じて、プレイヤーのパラメータが上昇します。

ヒロインは、プレイヤーのパラメータを参照して、好感度を評価します。ヒロインは複数人おり、それぞれ異なる評価関数で好感度を計算します。

プレイヤーが行動することができる期間は30日(1ヶ月)であり、プレイヤーはヒロインの好感度をできる限り上げるように行動を決定していきます。ただし、実際にあるゲームでは、特別なイベント(デート・病気・修学旅行等)があるかもしれませんが、今回のシミュレーションでは想定しておりません。

実行結果

上記の内容を踏まえ、今回の多目的遺伝的アルゴリズムを実行する上での条件を以下のようにしました。
※ ハイパーパラメータのチューニング等は行っておりません。ご了承ください。。。

  • 個体数:100
  • 遺伝子の長さ:30
  • 世代数:40
  • 交叉:二点交叉
  • 突然変異:10%
  • 評価関数(ヒロインの数):3
    • Aさん:理系パラメータを評価
    • Rさん:芸術パラメータを評価
    • Yさん:各パラメータを評価

世代数における評価値の推移は以下の通りになります。
ここでの評価値は各ヒロインの好感度の平均を算出しています。

スクリーンショット 2019-12-23 14.01.46.png

横ばいか少しの下降傾向にあるように見受けられます。評価値の平均は0.5で収束しているのでしょうか。評価関数の計算式を見直す必要があるかもしれません。

続いて、各個体の評価値を一部抜粋して確認していきます。

name Aさん Rさん Yさん
NQESC 0.508 0.508 0.234
RQEEZ 0.516 0.508 0.235
TSTWL 0.508 0.508 0.264

AさんとRさんはほぼ同じ好感度になっています。YさんはAさんとRさんの半分くらいになっていますね(生徒会長を攻略することは難しいのでしょうか。。。)

これらの結果から、各評価関数がパレート最適になっているだろう近似解を取得できているように見受けられます。つまり、クリスマスパーティーが開催されるのであれば、声がかかる可能性があるわけですね!

まとめ

今回は多目的遺伝的アルゴリズムで恋愛シミュレーションゲームのヒロインを同時攻略してみました。多目的最適化によって、複数のヒロインの好感度をパレート解に近い形で得られる可能性があることを確認することができました。

評価関数やハイパーパラメータのチューニングを改良していくことによって、プレイヤーがひとりぼっちになることを回避していけたらな、と思います。

P.S.

私事ではありますが、有志で技術書典8にサークル参加することになりました。
マイナーなプログラミング言語を中心に解説する本を頒布する予定です。
よろしくお願いいたしますm(_ _)m
スクリーンショット 2019-12-16 13.42.03.png

参考

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

Rails6 のちょい足しな新機能を試す113(MySQL enum set 編)

はじめに

Rails 6 に追加された新機能を試す第113段。 今回は、MySQL enum set 編です。
Rails 6 では、MySQL の enum や set のカラムの schema dump が正しく出力されるようになりました。

Ruby 2.6.5, Rails 6.0.2.1, MySQL 8.0.16 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)

$ rails --version
Rails 6.0.2.1

今回は、users テーブル (User モデル)に enumset のカラムを追加して試してみます。

Rails プロジェクトを作成する

$ rails new rails_sandbox
$ cd rails_sandbox

User モデルを作成する

name カラムだけもつ User モデルを作成します。

enumset のカラムは、後で、migration ファイルを直接編集して、追加します。

bin/rails g model User name

migration ファイルを編集する

migration ファイルを編集して、 enum カラムの generationset カラムの learning を追加します。

db/migrate/20191221002717_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name
      t.column :generation, "enum('baby', 'toddler', 'preschool', 'gradeschool', 'teen', 'young_adult')"
      t.column :learning, "set('piano', 'english', 'swimming', 'ballet', 'calligraphy')"

      t.timestamps
    end
  end
end

seed データを作成する

機能を試すためには、作る必要は、無いのですが、一応、 seed データを1件作成します。

db/seeds.rb
User.create(name: 'Dave', generation: 'gradeschool', learning: 'english,swimming')

マイグレーションを実行する

マイグレーションを実行します。
shell
$ bin/rails db:create db:migrate

スキーマファイルを確認する

できたスキーマファイルを確認します。

db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_21_002717) do

  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name"
    t.column "generation", "enum('baby','toddler','preschool','gradeschool','teen','young_adult')"
    t.column "learning", "set('piano','english','swimming','ballet','calligraphy')"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

end

Rails 5 では

Rails 5.2.4 では、スキーマファイルは以下のようになってしまいます。

generationlearning のカラムが string になってしまい、ENUM と SET であることがわからなくなってしまいます。

db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_21_005503) do

  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
    t.string "name"
    t.string "generation", limit: 11
    t.string "learning", limit: 41
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

MySQL で直接確認する

なお、MySQL で直接確認した場合は、Rails 6.0.2.1 も、Rails 5.2.4 も同じです。

mysql> show columns from users;
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| Field      | Type                                                                  | Null | Key | Default | Extra          |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| id         | bigint(20)                                                            | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)                                                          | YES  |     | NULL    |                |
| generation | enum('baby','toddler','preschool','gradeschool','teen','young_adult') | YES  |     | NULL    |                |
| learning   | set('piano','english','swimming','ballet','calligraphy')              | YES  |     | NULL    |                |
| created_at | datetime(6)                                                           | NO   |     | NULL    |                |
| updated_at | datetime(6)                                                           | NO   |     | NULL    |                |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

bin/rails db:reset を試す

bin/rails db:reset を実行してから、MySQL で直接確認すると

Rails 6.0.2.1 は、

mysql> show columns from users;
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| Field      | Type                                                                  | Null | Key | Default | Extra          |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
| id         | bigint(20)                                                            | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)                                                          | YES  |     | NULL    |                |
| generation | enum('baby','toddler','preschool','gradeschool','teen','young_adult') | YES  |     | NULL    |                |
| learning   | set('piano','english','swimming','ballet','calligraphy')              | YES  |     | NULL    |                |
| created_at | datetime(6)                                                           | NO   |     | NULL    |                |
| updated_at | datetime(6)                                                           | NO   |     | NULL    |                |
+------------+-----------------------------------------------------------------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)

と変わりないですが、Rails 5.2.4 では

mysql> show columns from users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| generation | varchar(11)  | YES  |     | NULL    |                |
| learning   | varchar(41)  | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

と変わってしまいました。

rails console からデータを検索する

(ここからはおまけです。)

bin/rails db:reset で seed データが登録されたので、検索してみます。

irb(main):006:0> User.where(generation: 'gradeschool')
  User Load (1.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`generation` = 'gradeschool' LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, name: "Dave", generation: "gradeschool", learning: "english,swimming", created_at: "2019-12-21 02:04:41", updated_at: "2019-12-21 02:04:41">]>
irb(main):007:0> User.where("learning like '%swimming%'")
  User Load (0.9ms)  SELECT `users`.* FROM `users` WHERE (learning like '%swimming%') LIMIT 11
=> #<ActiveRecord::Relation [#<User id: 1, name: "Dave", generation: "gradeschool", learning: "english,swimming", created_at: "2019-12-21 02:04:41", updated_at: "2019-12-21 02:04:41">]>

正しくないデータを投入する

generation や、learning に不正なデータを指定して登録すると ActiveRecord::StatementInvalid が発生します。

generation も learning も不正なデータの場合を試してみます。

最後のエラーメッセージが Data truncated for column 'generation' at row 1 であることから generation に問題があることがわかります。
(が、どうして truncate されたか、ちょっとわかりにくいです...。)

irb(main):008:0> User.create(name: 'NG', generation: 'NG', learning: 'NG')
   (0.5ms)  BEGIN
  User Create (1.6ms)  INSERT INTO `users` (`name`, `generation`, `learning`, `created_at`, `updated_at`) VALUES ('NG', 'NG', 'NG', '2019-12-21 02:15:29.767037', '2019-12-21 02:15:29.767037')
   (0.4ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):8
ActiveRecord::StatementInvalid (Mysql2::Error: Data truncated for column 'generation' at row 1)

generation は正しいが、 learning が不正なデータの場合です。

最後のエラーメッセージが Data truncated for column 'learning' at row 1 であることから learning に問題があることがわかります。

irb(main):009:0> User.create(name: 'NG', generation: 'gradeschool', learning: 'NG')
   (0.4ms)  BEGIN
  User Create (1.2ms)  INSERT INTO `users` (`name`, `generation`, `learning`, `created_at`, `updated_at`) VALUES ('NG', 'gradeschool', 'NG', '2019-12-21 02:15:50.398770', '2019-12-21 02:15:50.398770')
   (0.2ms)  ROLLBACK
Traceback (most recent call last):
        2: from (irb):9
        1: from (irb):9:in `rescue in irb_binding'
ActiveRecord::StatementInvalid (Mysql2::Error: Data truncated for column 'learning' at row 1)

Rails 5 では

bin/rails db:migrate で作成した場合は、Rails 6 と同じ動作ですが、 bin/rails db:reset すると Type がただの string になってしまうため、不正なデータでも登録できてしまいます。

ActiveRecord の enum との関係

ActiveRecord の enum と MySQL の enum は全く関係がなく、以下のように、 User モデルに enum を定義しても、MySQL の generation カラムの enum とは連動せず、期待通りの動作はしません。

app/models/user.rb
class User < ApplicationRecord
  enum generation: %i[baby toddler preschool gradeschool teen young_adult]
end

試したソース

https://github.com/suketa/rails_sandbox/tree/try113_mysql_enum

参考情報

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

Cable master とドント方式と中央値の中央値

はじめに

競技プログラミングで見つけたとある問題が見かけの違う別の問題と本質的に同じで、その最小計算量オーダが意外と小さかったというお話です。

Cable master 問題

その問題は「プログラミングコンテストチャレンジブック」 (通称「蟻本」。表紙に蟻の絵が描いてあるから) の中級編の問題
「Cable master」
(英語版の元問題はこちら

長さがそれぞれ $ L_i $ であるような $ N $本の紐があります。これらの紐を切って、同じ長さの紐を $ K $本作るときの最長の長さを求めなさい。
答えは小数以下2桁までを切り捨てて出力しなさい。

蟻本では二分探索を使って解の近似値を求めていますが、こういう問題を見たら近似値ではなく厳密な値を求めたくなるのが人間の性(さが)ですよね。

厳密な解はN本の紐のどれかをある本数で割った長さになるはずです。
なぜならちょうど割り切れた長さでなければまだ伸びしろがあるわけですから解が最長の長さであることに矛盾します。
つまり解は

\{ \frac{L_i}{j} \mid 1 \leqq i \leqq N,\, 1 \leqq j \leqq K \}

の中にあります。
この集合を長さ順にソートして順番にその長さで N本取れるか試していけば解が見つかります。
ナイーブにこの方法を行うと計算量オーダは大きいですが、工夫すると O(N * log(N)) の計算量で求められることがわかりました。(K に影響されない!)

Cable master 問題とドント方式

これが計算量の限界かなと思い答えがネット上に載っていないか探したところ、まさにこの問題を取り上げている
情報学会の会誌「情報処理」連載の「プログラム・プロムナード」の「ケーブルマスタ」
を見つけました。
そこには Cable master問題がドント方式で解けると書いてあります。

ドント方式とは Wikipedia の記事によると

政党名簿比例代表において、議席を配分するための最高平均方式(highest averages method)のひとつである。
この方式はベルギーの数学者ヴィクトル・ドント(Victor Joseph Auguste D'Hondt)から名づけられた。

(日本でも比例代表の選挙で使っています)

ドント方式の手順は
(上記 wikipedia の記事の「配分」の「1議席ずつ配分するのではなく次のように考えても同様である。」のところに書いてある方法)
(1) 各政党の得票数を1,2,3・・・の整数で割る
(2) 一人当たりの得票数が多い順(割り算の答えの大きい順)に、1議席ずつ各政党に議席を配分する
です。

例えば 9議席を A党、B党、C党の3党で争っていて各政党の獲得得票数がそれぞれ 120票、90票、60票だったとします。
まず、各政党の得票数を1,2,3・・・の整数で割ると

/ 1 / 2 / 3 / 4 ...
A党 120 / 1 120 / 2 120 / 3 120 / 4 ...
B党 90 / 1 90 / 2 90 / 3 90 / 4 ...
C党 60 / 1 60 / 2 60 / 3 60 / 4 ...

これらをまとめて値の順に並べて上位9個を取ると(どの党のものかわかるように後ろに党名をつけておきます)

  \{ 120(A), 90(B), 60(C), 60(A), 45(B), 40(A), 30(C), 30(B), 30(A)\}

結果、A党は4議席、B党は3議席、C党は2議席獲得することになります。

ドント方式が Cable master とどう対応するかというと

Cable master ドント方式
$ K $ 総議席数
$ N $ 党の数
$ L_i $ 各党の得票数
各紐から取れる紐の本数 各党の獲得議席数
答えである紐の長さ 最後に議席獲得したときの割り算の答え
(上記の例では 30)

という対応になります。

先ほどの Cable master の解法では

\{ \frac{L_i}{j} \mid 1 \leqq i \leqq N,\, 1 \leqq j \leqq K \}

を長さ順にソートして順番に N本取れるか試していけばよいと書きましたが、実は単に長さ順でN番目の値を取ってくればよいのでした。

Cable master問題がドント方式となぜ本質的に同じ問題なのかについては上記の「プログラム・プロムナード」で見ていただくとして次に進みましょう。

ドント方式を計算量 O(N) で解く

Cable master問題がドント方式と同じだとわかったので今度は 「ドント方式 計算量」 で検索すると、なんと最悪ケースでも計算量 O(N) で解けると書いてある論文を見つけました。

「議席配分法に対する線形時間アルゴリズム」
(ドント方式の計算量 O(N) のアルゴリズムが書いてあるのは、定理 4.2 です。)

なぜ O(N) で計算できるかは論文を見ていただく(ちょっと難しい)ことにして次に進みましょう。

計算量 O(N) で解くプログラム

上記の論文に書いてあるアルゴリズムにしたがってプログラムを書いてみましょう。Ruby で書いてます。

# s_total: 総議席数。論文では S
# vs: 各党の獲得投票数。論文では {v_i}
def dhondt(s_total, vs)
  # 計算しやすいように実数にしておく
  vs = vs.map(&:to_f)
  n = vs.length # 論文でも n
  vsum = vs.sum # 論文では Σ v_i

  # step1
  # 理想配分数
  ss = vs.map do |v| s_total * v / vsum end
  # 初期配分数
  s0s = ss.map(&:to_i)

  # step2
  # 追加候補集合
  cs = vs.map.with_index do |v_i, i|
    s_i = ss[i]
    s0_i = s0s[i]
    # 論文
    # a < 1 + n/S, a = s / s_i なので s < (S + n) * s_i / S
    (s0_i + 1 .. ((s_total + n) * s_i / s_total - 1).ceil).map {|s| s / s_i}
  end

  # step3
  # 不足配分数
  d = s_total - s0s.sum
  # 論文では α_dth
  a_dth = d == 0 ? 1.0: select_kth(cs.flatten, d-1)

  # step4
  # 追加配分数
  dss = cs.map do |as| as.take_while {|a| a <= a_dth}.count end
  # 各党の獲得議席数
  ss = s0s.zip(dss).map do |is, ds| is + ds end

  # Cable master問題では答えの紐の長さ
  z = vsum / s_total / a_dth

  [z, ss]
end

# ソートされていない配列からk番目に大きな数を求める
# k は 0オリジン
def select_kth(xs, k)
  # どう実装するか
end

ただし最後のメソッド select_kth は複雑なので少し解説してから実装することにします。
というのも上記の論文では、ソートされていない N個の数の集合の中から k番目に大きな数を最悪ケースでも計算量 O(N) で得ることを求めているのです。
すぐに思いつく方法は配列をソートして上から k番目の数を求める方法ですが、この方法ではソートするのに O(N*log(N)) かかってしまいます。

最悪ケースでも O(N) の計算量で k番目の数を求めるアルゴリズは見つかっています。(introselect という名前のようです)

パーティションベースの汎用選択アルゴリズムの中の「中央値の中央値」を用いたクイックセレクトにアルゴリズムが書かれています。

クイックソートと似たクイックセレクトという手法を使っていますがクイックソートと同様にピボットの選び方に失敗すると最悪ケースの計算量オーダが大きくなるのでさらに中央値の中央値という手法を使って計算量が大きくならないように工夫しています。

このアルゴリズムを使って select_kth を実装しましょう。

def select_kth(xs, k)
  return xs[0] if xs.length == 1

  pivot = select_pivot(xs)

  lows = xs.find_all do |x| x < pivot end
  lows_len = lows.length
  if k < lows_len
    select_kth(lows, k)
  elsif k == lows_len
    pivot
  else
    highs = xs.find_all do |x| x > pivot end
    select_kth(highs, k - (lows_len + 1))
  end
end

def select_pivot(xs)
  # 中央値の中央値を選ぶ
  median_of_medians(xs)
end

def median_of_medians(xs, i = xs.length/2)
  return median5(xs, i) if xs.length <= 5

  medians = xs.each_slice(5).map do |s| median5(s) end

  pivot = median_of_medians(medians)

  lows = xs.find_all do |x| x < pivot end
  lows_len = lows.length
  if i < lows_len
    median_of_medians(lows, i)
  elsif i == lows_len
    pivot
  else
    highs = xs.find_all do |x| x > pivot end
    median_of_medians(highs, i - (lows_len + 1))
  end
end

def median5(xs, i = xs.length/2)
  return xs.sort[i]
end

これでめでたしと思ったら

と、ここまで書いておいてなんですが、「「中央値の中央値」を用いたクイックセレクト」に

ただし、この方法では最悪時間は確かに線形になるが、平均時間は、実際にはピボット値を無作為に選ぶなどの素朴な方式の方が優れている。

と書いてあるとおり実際にはピボット選択に「中央値の中央値」を使わずに

def select_pivot(xs)
  # 要素をランダムに選ぶ
  xs.sample
end

とするのがよいでしょう。

さらに言えば現実の問題では O(N) も O(N * log(N)) も似たようなものなので Wikipedia のドント方式の記事の「配分」の「次のようにして1議席ずつ議席を配分する」に書いてある方法(最初に紹介した「情報処理」連載の「プログラム・プロムナード」にプログラムが載っています)を使うのが一番楽ではないでしょうか。

最後はちょっと身も蓋もない結論になってしまいましたがドント方式の理論上の最小計算量にふれて面白かったのではないでしょうか。

これで(多分)本当にめでたしめでたし。

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

新卒エンジニアの成長tips(6個)

新卒エンジニアとして入社して8~9ヶ月経過して、rubyなら少しは慣れたような気がしたので、今の気持ち(ポエム)を書こうかなと思い立ちました?

以下のような新卒エンジニア向けの記事です!
・本格的な中〜大規模のrailsは初めてで
・エンジニアインターンでGitの使い方を知っていて
・railsを少し触った事があって
・国籍がコロンビアである??

以下に限ったことではないのですが、僕の中で実践して良かったなと思った事だけなので?

これはrubyに関するtipsがメインです

メソッド名をググらない

method名すぐ忘れますよね
methods.grep()を使うと良いと思いました!

> Date.today.methods.grep /begin.*day/
=> [:beginning_of_day, :at_beginning_of_day] 

rspecを書く

特に大事なのはrspecを一番書ける人と仲良くなって、rspecの書き方を教えてもらった事です!
初めてのrpsecは書き方のイメージがつかないので、とりあえず書いてレビューしてもらおう?
結果的に仕様を再確認したり、自分のコードのバグを見つけられます!

Better Specs は初めて読むと何とも思わないけど、色々と自分で書いたら、意味がわかったし、学びもあった!

次のステップとして、rspecのレビューをするがあると思います。
スラスラ書けても、スラスラレビューできないのがrspecなんだなと思いました!
(書き方が色々あったり、裏技があったり、ややこしいんですよね?)

同じ実装を個人開発でも作ってみる

会社の技術をコピペしろということではなく、同じ機能を自分で実装するのです。
結果と手順は知っているので、調べながら、どうしてもわからない場合は出勤した時にコードを少し読んで、どんなライブラリを使っているのか確認しましょう?
僕はこれでrescueの仕組みや、DeviseTokenAuthの仕組みを勉強しました!

0→1にフェーズでしか実装しない(その後、改修されない)仕組みは自分から学ぼうとしないと、わからないので、大事だと思いました!

gemのソースコードを読む

rubyのgemは素晴らしいです。
とにかく読みやすい!!?
また、普段何気なく使っているが意図がわからなかったdo~endブロックの挙動とかもわかります!
PR → 【ruby】簡単ッ! do~endブロックって?
何より、機能を見に行けばqiitaには書いてないmethodを見つけることもできます。

とにかくsource_locationを使おう!

> User.method(:take).source_location
 => ["/Users/****/Desktop/****/vendor/bundle/ruby/2.5.0/gems/activerecord-5.1.6.1/lib/active_record/querying.rb", 3]

端から読むのでなく、興味のある所だけ摘むだけでokだと思います。
将来的には全部知りたいですね。

Object(オブジェクト)を意識する

hoge.classでクラス名を知るだけで検索効率が全然違います。
何より、わからなければソースコードを読めば良いのです?

> Array.new.each.class
=> Enumerator 

とにかくレビューする

これはrubyに関わらないことなんですが、、
やっぱり身近にいる優秀なエンジニアのコードを読むのが一番良いです☺️
もちろん自分で書いてレビューしてもらうのも大事ですが、レビューしないと視野が広がらないと思いました。
とにかく、レビューして自分では思いつかない箇所をその都度勉強すべし!

所感

レビューの話と似たようなことなのですが、とにかく優秀な先輩の仕事は全てストーキングしてます?️‍♀️
先輩のslackは全てスレッドフォローして普段からも疑問は質問して、勝手に学ぶのが一番近道だなと思います。

以上です!
来年は、もっと成長してたら良いな〜?

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

codeanywhereでgitにハロワをプッシュしよう!

前置き

codeanywhereを使って、スマホのみでgitにRubyのハロワをプッシュする流れについて解説します。

手順

まずはハロワの実行ファイルを作成します。
手順は↓を参照して下さい。
Codeanywhereを使ってスマホでプログラミングをしよう

githubにて、リポジトリを作成します。

Repositories>Desktop versionをタップ。
3C80F71A-8BD3-49F5-A99B-E649A680729A.jpeg

Newをタップ
AFDD3438-E1F9-4211-A944-B81267A92FF2.jpeg

下記のようにリポジトリを作成します。
C9A43831-B59B-4E21-8A22-574288AF2BD9.jpeg

SSH画面にて、下記のリンクの通り操作します。

https://techacademy.jp/magazine/6235

結果はこんな感じです。
959B207B-79E5-4E44-906B-97A994FD8EB1.png

下の通り、ソースがgitにプッシュされています。
594BD42D-A52A-4784-9115-4C8D6144C1B2.png

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

Sidekiq 6の新機能・変更点

はじめに

Ruby on Rails (以下Rails) でバックグラウンドジョブを実行する際によく使用されるGemである、Sidekiqのバージョン6.0が今年(2019年)9月にリリースされました。このバージョンでは、Active Jobへのオプション指定などの機能強化と共に、パフォーマンス向上が行われ、sidekiqctlコマンドが廃止されるなどの大きな変更が行われています。本記事では実際に使用した経験を元に、Sidekiq 6の新機能や変更点について説明を行います。

注意点

本記事で取り扱うエディション

本記事ではGitHubで公開しているOSS版のSidekiqについて記述しています。Pro / Enterprise版を使用している場合は、公式サイト(英語)を参考にしてください。

サポートされるRails / Ruby / Redisのバージョン

サポートされるバージョンは以下の通りになります。

  • Ruby on Rails 5以上
  • Ruby 2.5以上
  • Redis 4以上

上記より古いバージョンを使用している場合は、アップグレードを事前に行う必要があります。

新機能

Active JobでSidekiq独自のオプションを指定可能に

ActiveJob::Baseを継承したジョブで使用する際に、sidekiq_optionsを指定することで、Sidekiq独自のオプションを一部使用できるようになりました。

以下はリトライの回数と、Web UIで表示するバックトレースの行数をsidekiq_optionsで指定しています。

class MyJob < ActiveJob::Base
  queue_as :myqueue
  sidekiq_options retry: 10, backtrace: 20
  def perform(...)
  end
end

以下が使用できるオプションの一覧となります。

オプション 内容
retry 整数 リトライ回数を指定する
backtrace true, false, 整数 SidekiqのWeb UIにバックトレースを表示するかどうか
整数の場合は指定した行数で表示する

参考

ログの出力形式を選択可能に

ログの出力機能がプラグイン化され、出力形式を選択できるようになりました。例えば、JSON形式の出力を行う場合は、以下のようにSidekiq::Logger::Formatters::JSONを指定します。

Sidekiq.configure_server do |config|
  config.log_formatter = Sidekiq::Logger::Formatters::JSON.new
end

以下が使用できるログの出力形式の一覧となります。

フォーマッタ 説明
Sidekiq::Logger::Formatters::Pretty 通常の出力
Sidekiq::Logger::Formatters::WithoutTimestamp タイムスタンプなしで出力
Sidekiq::Logger::Formatters::JSON JSON形式で出力

ログ出力の例

Sidekiq::Logger::Formatters::Pretty

2019-08-31T15:36:07.569Z pid=82859 tid=11cy9br class=HardWorker jid=528f1b0ddc4a9d0690464fe4 INFO: start

Sidekiq::Logger::Formatters::WithoutTimestamp

pid=82859 tid=119pz7z class=HardWorker jid=b7f805c545c78770d30dc1fd elapsed=0.089 INFO: done

`Sidekiq::Logger::Formatters::JSON

{"ts":"2019-09-01T22:34:59.778Z","pid":90069,"tid":"104v8ph","lvl":"INFO","msg":"Running in ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]"}

参考

ジョブ毎のログレベル指定が可能に

ワーカーの.setメソッドの引数でlog_levelを指定することで、特定のジョブのログの出力レベルを指定できるようになりました。

MyWorker.set(log_level: :debug).perform_async(...)

SidekiqはデフォルトでRubyの標準ライブラリのLoggerを使用しており、log_levelで指定できるのは、:unknown:fatal:warn:info:debugになります。

参考

ジョブのタグ付けが可能に

ワーカーのクラスのsidekiq_optionstagsを指定することで、ジョブのタグ付けができるようになりました。

class MyWorker
  include Sidekiq::Worker
  sidekiq_options tags: ['bank-ops', 'alpha']
  ...
end

参考

Web UIでのダークモードのサポート

ダークモードがサポートされたブラウザ(IEやEdge、Operaは非サポート)でモードが有効な状態でWeb UI(Sidekiqの管理画面)にアクセスした場合は、黒ベースの画面で表示するようになりました。

参考

変更点

sidekiqctlコマンドの削除

Sidekiqプロセスを停止、終了するために用いられてきた、sidekiqctlコマンドが廃止されました。Sidekiq 6では、このコマンドの代わりにkillコマンドでシグナルを送ることで、プロセスの停止、終了を行うことができます。

廃止されたコマンド 代替となるコマンド 説明
sidekiqctl quiet kill -TSTP <pid> ワーカーの各スレッドを停止
sidekiqctl stop kill -TERM <pid> (上記コマンドの実行後に)プロセスを終了

デーモン化を廃止し、サービスとして実行するように

サービスの管理をOS側(Systemd、Upstartなど)で行うようにするために、デーモン化を廃止しました。このため、本番環境などサーバーで動かす場合は、Sidekiqのサービスの設定ファイルを新たに作成し、サービスを有効にする必要があります。 

デーモン化の廃止に至った背景に関しては、Sidekiq作者による説明が以下のページにあります。

上記の記事では、以前から使用されてきた長時間動作するデーモンプロセスが、現在ではSystemdを使用したサービスに置き換えられていて、このことによって、ロギングやクラッシュ時の再実行などの処理を自前で実装することがなく、より堅牢な実行が可能になった、ということが書かれています。

Sidekiqのサービスの設定、起動や停止コマンドに関しては、この記事の次の節のアップグレードの部分を参考にしてください。

logfilepidfileコマンド引数の廃止 

上記のデーモン化の廃止に伴って、ログの出力や、プロセスIDの保持をSystemdなどのサービス側で行うようになったので、sidekiqコマンドからlogfilepidfileコマンド引数が削除されました。 

デフォルトのシャットダウン時間が8秒から25秒に変更に

HerokuのDynoとAmazon ECS コンテナがアプリケーションのシャットダウンで30秒のタイムアウトを使用するようになったため、Sidekiqも8秒から25秒にタイムアウト時間を伸ばしました。以前の挙動に戻すには、オプション-t 8sidekiqコマンドに対して指定します。

REDIS_PROVIDER環境変数の検証を行うように

REDIS_PROVIDER環境変数は、RedisサーバーのURLを保持する他の環境変数を指定するのに使用します。もし、REDIS_PROVIDER変数に直接URLなどの不適切な文字列な文字列が指定されている場合は、以下のような警告を出すようになりました。

REDIS_PROVIDER should be set to the name of the variable which contains the Redis URL, not a URL itself.
Platforms like Heroku will sell addons that publish a *_URL variable.  You need to tell Sidekiq with REDIS_PROVIDER, e.g.:

参考

パフォーマンスの向上

生成するオブジェクトを減らすなど、細かいコードのチューニングを行うことによって、Sidekiq 5系列と比較して、10-15%実行速度が向上しているそうです。

参考

アップグレード方法

Gemのアップグレード

最初に、Gemfileに以下のように指定して、bundle updateを実行して、Sidekiq 5系列の最新版にアップグレードします。

gem 'sidekiq', '< 6'

Sidekiqを動かしてみて、非推奨の警告(Deprecation warnings)が出ないようにコードの修正を行います。

修正が完了したら、Gemfileに以下のように指定して、再度bundle updateを実行して、Sidekiq 6.xへアップグレードします。

gem 'sidekiq', '< 7'

サービスの設定ファイルの作成

変更点で述べたように、本番環境では、Sidekiqプロセスはデーモンでなくサービスとして起動するようになったため、設定ファイルを作成する必要があります。

開発環境では、これまで通りユーザーがsidekiqコマンドを実行してフォアグラウンドで起動し、ログは標準出力および標準エラー出力に送信されるため、サービスとして起動させる必要はありません。

SystemdでSidekiqをサービスとして実行するには、以下のファイルを元に設定ファイルを生成し、サーバーの適切な場所(CentOSの場合は/usr/lib/systemd/system、Ubuntuの場合は/lib/systemd/system)に配置する必要があります。

https://raw.githubusercontent.com/mperham/sidekiq/master/examples/systemd/sidekiq.service

ファイルの配置を行ったら、以下のコマンドでサービスを有効にします。

$ sudo systemctl enable sidekiq

サービスの起動と停止

設定ファイルを配置後は、以下のコマンドでサービスの起動と停止を行います。

サーバーの起動

$ sudo systemctl start sidekiq

サーバーの停止

$ sudo systemctl stop sidekiq

参考URL

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