20191209のRubyに関する記事は25件です。

ConoHaVPSのDokkuテンプレートでWebアプリを楽々デプロイ

はじめに

この記事はConoHa Advent Calendar 2019 18日目の記事です。

こんにちは、高校生プログラマーのいっそです。
普段は趣味でRubyを用いたWebアプリケーションの製作をしています。
今回Qiita初投稿&Advent Calendar初参加なので、このはちゃんとこうしてクリスマスまでカウントダウンができるのをとても嬉しく思いますし、ちょっと緊張もしてます...。
とにかく!うまくフィーチャーできるように頑張るので今回はよろしくお願いします!

本題

今回は「ConoHa」VPSのテンプレートイメージを使って「Dokku」を構築し、そこに実際に製作したWebアプリケーションをデプロイしていきたいと思います。
あと、デプロイした「このはちゃんに愛を叫びたい」というWebサイトでこのはちゃんに「愛」を叫ぶってのもやりたいと思います。

なぜ構築するか

  • 自分でカスタマイズ可能なPaaSを持てる
  • 本番環境のWebアプリケーションを簡単にデプロイできる
  • Always Onで訪問者の待機時間を減らせる
  • 独自ドメインの設定やSSL設定などをクレジットカードなしでできる(PaaSホスティングだとクレジットカードが必須で高校生の立場ではほぼ不可能)
  • ConoHaは爆速でWebアプリケーションとの相性が抜群!
  • ConoHaのテンプレートイメージだから安定してるし、何より構築が楽
  • 以下の方法で行うとWebアプリを何個も建てれる!

Dokkuを構築してみる

ConoHaVPSダッシュボード左上のサーバー追加をクリック。
リージョンとVPSのスペックはお好みで(私は高校生でお金がないので、手軽な東京の1コアCPU、メモリ512MB、SSD20GBを選択しました)。
次にイメージタイプですが、アプリケーションタブDokkuを選択します。
あとはお好みのrootパスワード、ネームタグ(サーバーの名前)を付けます。
追加をクリックすると20秒程でサーバーが作られます。
実質これがPaaSを建てたってことで!超簡単ですね!

Dokkuの設定

次に実際に使えるように設定していきます。
サーバーリストから先ほど追加したサーバーのネームタグを選択。
ネットワーク情報のIPアドレスにブラウザでアクセスします。
するとDokku Setupと言う画面が出ます。

Admin Accessにはデプロイ元のパソコンの公開鍵を入力します。
公開鍵は~/.ssh/id_rsa.pubと言うファイルのssh-rsaなどから始まる文字列を使用します。

HostnameにはConoHaのIPアドレスを指定します。
IPアドレスが"-"で区切られてるので、それを"."に置き換えれば大丈夫だと思います。

Finish Setupをクリックして設定を終了します。
これでDokkuのデプロイを受け取る設定は終わりました。

今回デプロイするWebアプリの紹介

ogp.png
今回私が製作したのは「このはちゃんに愛を叫びたい(通称:このさけ)」です!
Webアプリとしてはシンプルでこのはちゃんへの愛を投稿するシステムになってます。
シンプルな分読み込みのパフォーマンスも高く、デスクトップVerのPageSpeed Insightsでは常に95%以上を叩き出すくらい速いです。
スクリーンショット 2019-12-06 17.13.59.png
このアプリとConoHaのVPSとは非常に相性が良く、VPSの速さを皆さんのブラウザでも体験してもらえるのではないかなと考えています。

いよいよWebアプリをデプロイ

さて話を本題に戻し、「このさけ」をデプロイしていきたいと思います。

まずはDokku上にアプリのコンテナ(格納場所)を作成します。

Create_App
# Dokkuホスト上で
#アプリケーション名は好きな名前を付ける
dokku apps:create アプリケーション名
#dokku apps:create conosake

次にデータベースにPostgresを使用するのでプラグインのインストールデータベースコンテナの作成をします。

Make_Database
# Dokkuホスト上で
sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git

#データベース名はアプリケーション名と違う名前を付ける
dokku postgres:create データベース名
#dokku postgres:create conosakedatabase

そしてこれまで作ったアプリとデータベースを関連付けしていきます。

Link_App&Database
#Dokkuホスト上で
dokku postgres:link データベース名 アプリケーション名
#dokku postgres:link conosakedatabase conosake

これでデプロイされたアプリを受け取る準備が整いました。
ローカルでのデプロイ作業に移ります。
デプロイにはGitを使用していきます。

Deploy_App
#ローカルで
cd アプリの場所

git init
git add -A .
git commit -m 'First deploy'

git remote add dokku dokku@VPSのIPアドレス:アプリ名
#git remote add dokku dokku@123.456.789.012:conosake

git push dokku master

yes/noが出てきたら全てyesにしましょう。

=====> Application deployed:
       http://123.456.789.012:34567

To 123.456.789.012:アプリケーション名
 * [new branch]      master -> master

となればデプロイ完了です。
しかし、このままでは"Internal Server Error"と表示されてしまうのでDokkuのデータベースでマイグレーション処理を行います(そのまま表示され、正常に稼働しているならこの設定は必要ないかもしれません)。
dokku run アプリケーション名の後にコマンドを入力するとDokku上でコマンドを実行できます。
データベースを最新のものにしましょう。

Database_Migration
#Dokkuホスト上で
#例はrakeを用いた際のマイグレーションコマンド。適宜読み替えてください。

dokku run アプリケーション名 マイグレーションコマンド
#dokku run conosake rake db:migrate

ローカルコンソール上の=====> Application deployed:の下に表示されているURLにブラウザでアクセスします。
これであなたのアプリDokku上で使えるようになりました!おめでとうございます?

Webアプリの設定をする

このままではIPアドレスがモロに出ていて、またSSLも適用されていないのでWebアプリとしてはかなり致命的な状況になっています。
しかしDokkuでは独自ドメインの設定やLet's Encryptを用いたSSL認証が簡単に行えます!
今回はそこまでやってみたいと思います。

独自ドメインの設定

まずドメインのDNSレコードを設定します。
AレコードでVPSのIPアドレスを指定し、追加します。

次にDokkuでアクセスできるように設定していきます。

Add_Domain
#Dokkuホスト上で
dokku domains:add アプリケーション名 ドメイン名
#dokku domains:add conosake conosake.isso.cc

これでアプリケーションと独自ドメインが接続されました!

Let's Encryptの設定

こちらはdokku-letsencrypt (Beta)というプラグインを使用して設定します。

Set_Let'sEncrypt
#dokkuホスト上で
sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

dokku config:set --no-restart アプリケーション名 DOKKU_LETSENCRYPT_EMAIL=e-mailアドレス
#dokku config:set --no-restart conosake DOKKU_LETSENCRYPT_EMAIL=mail@example.com

dokku letsencrypt アプリケーション名
#dokku letsencrypt conosake

dokku letsencrypt:auto-renew

これでSSL証明書も設定されましたし、自動で更新してくれる設定にもなりました!

いくつでもアプリをデプロイ可能に

実は前述の「いよいよWebアプリをデプロイ」から「Let's Encryptの設定」までを繰り返すことでいくつでもアプリをデプロイすることができます。
仕組みとしては全てのコンテナにポート番号が割り振られているためです。
ドメインもアクセスしたドメインを検知して自動的にアプリとつなげているのだと思います。
今回は詳しくは扱いませんが参考にしてみてください。
因みにこれを行うためにはスペックの良いサーバーを使うようにしてください。
管理するアプリが多すぎて処理しきれない場合があります。

早速愛を叫びます。

早速、ある意味本題のこのはちゃんに「愛」を叫ぶってことをやっていきたいと思います。ezgif.com-video-to-gif.gif
私は控えめに叫びましたが、皆さんはぜひもっともっと熱く叫んでください!
投稿は以下で行うことができます!
このはちゃんに叫びたい

さいごに

今回はConoHaVPSのDokkuテンプレートを用いてPaaSの構築を行ってみました。

初めての記事ちゃんと書けてたちょっと心配です.....。
けれど、少なからずConoHaのアプリケーションテンプレートの利便性と、自分でPaaSを作って自分のアプリをデプロイする楽しさってのは伝わったんじゃないかなって思ってます。
だからこの記事を読んでる皆さんも是非この楽しさを1回体感してみてください!

来年も参加できる機会があれば参加したいと思います!
どうもありがとうございました!

p.s.
何かあればお気軽にコメントよろしくお願いします。
是非Twitterなどにも足を運んでいただけると励みになります?

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

フィボナッチ数列を計算する

計算量について考えるネタとして、フィボナッチ数列を取り上げます。なお、多倍長整数など環境的にちょうどいいのと、作者が慣れているという理由で、コードはRubyで書きます。

いちおう、紹介

改めて説明するまでもないかもしれませんが、フィボナッチ数列とは以下のようなものです1

$F_1 = F_2 = 1,\ F_{n + 2} = F_n + F_{n+1}\ (n \ge 1)$

具体的に書き出すと、1、1、2、3、5、8、13、21のようになります。

ナイーブに実装

いちばんシンプルにやるなら、定義どおりに再帰的に実装することも可能です。

def fib(n)
  return 1 if n <= 2
  fib(n - 1) + fib(n - 2)
end

もちろんこれでも実行はできますが、文字通りねずみ算式にfibを呼び出し続けるため、計算量は $O(2^n)$ となってしまいます2。大きい数については実用的ではありません。

メモ化を行う

いちばんシンプルには、すでに計算した分をメモ化すれば、残りの計算が不要となるので、計算量は $O(n)$ まで下がります2

Rubyの場合、ブロックつきHashを使えば、物凄くシンプルにメモ化を書けます(参考)。

fib = Hash.new do |h, n|
  if n <= 2
    1
  else
    h[n] = h[n-1] + h[n-2]
  end
end

一般項で計算する

$x^2 + x - 1 = 0$ の解となる $\phi = (-1 +\sqrt{5})/2$ という値を使うと、$\phi(1 + \phi) = 1$ なので、漸化式は以下のように書き直せます。

\begin{align}
F_{n + 2} &= F_{n+1} + F_n \\
F_{n + 2} + \phi F_{n + 1} &= (1 + \phi)F_{n+1} + F_n\\
&= (1 + \phi)(F_{n+1} + \phi F_n)\\
&= (1 + \phi)^{n} (F_2 + \phi F_1)\\
&= (1 + \phi)^{n+1}
\end{align}

同様に、 $\phi' = (-1 -\sqrt{5})/2$ についても、 $F_{n + 2} + \phi' F_{n + 1} = (1 + \phi')^{n+1}$ となります。よって、この2式から $F_{n + 2}$ を消去して、

\begin{align}
(\phi - \phi') F_{n+1} &= (1 + \phi)^{n+1} - (1 + \phi')^{n+1} \\
F_{n+1} &= \frac{(1 + \phi)^{n+1} - (1 + \phi')^{n+1}}{\phi - \phi'}\\
&= \frac{1}{\sqrt{5}}\left\{\left(\frac{1 + \sqrt{5}}{2}\right)^{n+1} - \left(\frac{1 - \sqrt{5}}{2}\right)^{n+1}\right\}
\end{align}

さらに、 $((1 - \sqrt{5})/2)^n / \sqrt{5}$ の項の絶対値は0.5以下なので、1項目だけ計算して四捨五入すれば正しい値が出ます。

もっとも、大きなフィボナッチ数を計算する場合、概数はこれで見積もれますが、大きな数の計算精度に限界があるので、1の位まで正確な値を出すには向かない方法です。

行列で計算する

フィボナッチ数列の漸化式を行列で表すと、以下のようになります。

\begin{pmatrix}
F_{n + 1} \\ F_n
\end{pmatrix} = 
\begin{pmatrix}
1 & 1 \\
1 & 0
\end{pmatrix} 
\begin{pmatrix}
F_{n} \\ F_{n - 1}
\end{pmatrix} 

ということで、繰り返し展開を続けると、

\begin{pmatrix}
F_{n + 1} \\ F_n
\end{pmatrix} = 
\begin{pmatrix}
1 & 1 \\
1 & 0
\end{pmatrix}^{n - 1}
\begin{pmatrix}
F_{2} \\ F_{1}
\end{pmatrix} 

となります。あとは行列の冪を計算するだけですが、これは $O(\log n)$ で片付きます2

require 'matrix'

base = Matrix[[1, 1], [1, 0]]

p base ** 10

  1. 0からスタートする流儀と1からスタートする流儀があるようです。 

  2. 厳密にいえば、桁数が増えると加算・乗算の計算量自体も増えていきますので、このとおりではないです。 

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

rails-tutorial第14章

ユーザーをフォローする

まずはRelationshipモデルを作っていこう

$ rails generate model Relationship follower_id:integer followed_id:integer

作成されたマイグレーションファイルにインデックスを書き足していく。

db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

上二つは高速化のためのインデックスである。
最後は一意性を担保するためのインデックス。
これはfollower_idとfollowed_idの組み合わせは一つしかないよーって意味。

これで2回同じユーザーをフォローするとかはできなくなる。

UserとRelationshipの関連付け

まずは以下を見てみよう

app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

belongs_to :followerとすると、Relationshipのfollower_idとFollowerクラスのidカラムを結びつけますよーという意味になる。

ただ、結びつけたいのはUserクラスのidカラムなので、オプションでclass_name: "User"とすることで、follower_idとUserクラスのidカラムが結びつく。

では、Userモデルのファイルにはどのように書けば良いのだろうか?

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  .
  .
  .
end

まず、has_many :active_relationshipsとすると、active_relationshipsクラスを探そうとするが、そんなクラスはないのでオプションでclass_name: "Relationship"と指定している。逆に言えば、active_relationshipsの部分はなんでも良いのである。

次は、foreign_key: "follower_id"の部分だ。
これを指定しないと、UserクラスのidカラムをRelationshipクラスのどのカラムと関連づけるのかがわからなくなってしまう。(デフォルトではuser_idカラムと関連付けようとする)

なので、外部キーとして、Userクラスのidカラムと関連づけるカラムを指定してあげている。

これにより、@user.active_relationships
というような参照方法が可能になる。

また、@relationship.follower
という参照方法も可能になるってこと。

逆に言えば、belongs_toやhas_manyはメソッドを定義するメソッドであるとも言える。
(このケースだと、active_relationshipメソッド、followerメソッド)

なので、@user.active_relationships.first.followed
とすると、@userが最初にフォローしたユーザーが返ってくるということになる。

ここで全く関係ない余談

&.(ぼっち演算子)はレシーバーであるオブジェクトに対してあるメソッドを実行した時、そのオブジェクトがnilの場合、nilを返すことでエラーを出さなくしています。&.(ぼっち演算子)とはレシーバーであるオブジェクトがnilでなければそのまま結果を返し、nilの場合はnilを返すメソッドなのです。

もっと簡潔に

@user.active_relationships.first.followedだと長いのでどうにかできないか

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :following, through: :active_relationships, source: :followed
  .
  .
  .
end

これは、@user.active_relationships.followedを

followingメソッドを定義することにより、

@user.followingというように簡潔にかけるようにする。
(active_relationshipsメソッドを経由してfollowedメソッドを実行するのをfollowingと名付けますよーってこと)

relationshipモデルのバリデーションのテストをする

test/models/relationship_test.rb
require 'test_helper'

class RelationshipTest < ActiveSupport::TestCase

  def setup
    @relationship = Relationship.new(follower_id: users(:michael).id,
                                     followed_id: users(:archer).id)
  end

  test "should be valid" do
    assert @relationship.valid?
  end

  test "should require a follower_id" do
    @relationship.follower_id = nil
    assert_not @relationship.valid?
  end

  test "should require a followed_id" do
    @relationship.followed_id = nil
    assert_not @relationship.valid?
  end
end

次はバリデーションを設定する

app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

さらに、生成されたRelationship用のfixtureでは、マイグレーション (リスト 14.1) で制約させた一意性を満たすことができません。ということで、ユーザーのときと同じで (リスト 6.31でfixtureの内容を削除したように)、今の時点では生成されたRelationship用のfixtureファイルも空にしておきましょう

ちなみにテストが全て落ちるというときは、fixtureを見た方が良い。
そもそも、fixtureのサンプルデータが間違っており、dbにそれを伝えたことで全てのテストが落ちるらしい。

これでテストは通る。

その他のメソッド定義

次にfollowingなどの便利メソッドをテストする

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael = users(:michael)
    archer  = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

この状態では、まだ定義してないメソッドがあるので、それを定義していかないといけない。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  def feed
    .
    .
    .
  end

  # ユーザーをフォローする
  def follow(other_user)
    following << other_user
  end

  # ユーザーをフォロー解除する
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # 現在のユーザーがフォローしてたらtrueを返す
  def following?(other_user)
    following.include?(other_user)
  end

  private
  .
  .
  .
end

少しわかりにくいのは、

def follow(other_user)
  following << other_user
end

これは、そもそもfollow(other_user)メソッドがインスタンスメソッドなので、

self.following << other_user

の省略形だったことがわかる。
また、self.followingはフォローしているユーザーの配列を返すので、<<で問題ない。

ただ、どうやってrelationshipインスタンスが生成されるか、dbに保存されるかわからないので、安ラボの動画で書いてあるコードに変更。

def follow(other_user)
  self.active_relationships.create(followed_id: other_user.id)
end 

また、

def following?(other_user)
  following.include?(other_user)
end

include?メソッドは配列の要素に引数が含まれているかを判断してくれるメソッド。

余談:ダックタイピング

改良前

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      case employee
      when Staff   then employee.work
      when Manager then employee.work
      end
    end
  end
end

class Staff
  def work
    do_clean_up
  end
  def do_clean_up
    # ...
  end
end

class Manager
  def work
    do_check
  end
  def do_check
    # ...
  end
end

ダックタイピングを使うと、

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      employee.work
    end
  end
end

class Staff
  def work
    do_clean_up
  end
  def do_clean_up
    # ...
  end
end

class Manager
  def work
    do_check
  end
  def do_check
    # ...
  end
end

when case文がいらなくなったし、workメソッドを持っているクラスであれば、work(employees)メソッドを使えるようになった。

workメソッドを持っているということが、「ガーと鳴けば」に当たるのでは?

フォロワーを考える

これはフォローの時と逆のことをすれば良い

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
  .
  .
  .
end

これで自分のフォロワーが@user.followersで参照できるようになったので、
テストを書いていこう。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "should follow and unfollow a user" do
    michael  = users(:michael)
    archer   = users(:archer)
    assert_not michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    assert archer.followers.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end
end

UIを実装していこう

これでメソッドなどは定義できたので、UIを実装していく。

まずはフォローのサンプルデータを作るために

db/seeds.rb
# リレーションシップ
users = User.all
user  = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }

ちなみに、followingとfollowersはローカル変数?

$ rails db:migrate:reset
$ rails db:seed

ルーティング設定

次にフォローしてる一覧ページとフォローされてる一覧ページを作るためのルーティングを設定していく。
urlは
/users/1/following や /users/1/followersのような形にしたい。

それを踏まえると、

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end

memberメソッドを使うと、
/users/:id/...
この...に何を入れますか?というのを書き足すことができる

じゃあ、followingアクションとfollowersアクションはどこに作るの?
Usersコントローラに書けば良い。

statsパーシャルを作る。

次にフォロワーなどの統計情報を表示するstatsパーシャルを作る。

app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

そしたら、できたパーシャルをhomeページ(ログイン時)と、プロフィールページに挿入しよう。

その前に、なぜ<% @user ||= current_user %>のようなコードになるか見ていこう。

これは、static_pageコントローラのhomeアクションとuserコントローラのshowアクションで定義されているインスタンス変数が異なるからである。

showアクションには@userが定義されているが、static_pageコントローラでは定義されていない。なので、@userがいなければcurrent_userメソッドを呼び出すようにしている。

また、current_userメソッドはapplicationコントローラにmoduleをincludeしているので使える。

では、実際に埋め込んでいこう

```app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
        <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

次にcssを整えてあげる

app/assets/stylesheets/custom.scss
.
.
.
/* sidebar */
.
.
.
.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

/* forms */
.
.
.

フォローボタンの設置

これは、自分以外のユーザーのプロフィールページで、フォローしてなかったらフォローボタンが、フォローしたらフォロー解除ボタンが表示されるようにする。

これをdryに書くにはフォローボタンをそのままパーシャルにすると便利。

具体的には、フォローボタン、アンフォローボタン、その二つをif文で表示するfollow_formパーシャルの計3つを作る。

app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

<% unless current_user?(@user) %>
はもし@userが自分と同じなら、フォローボタンを表示しないようにする。

次はfollowパーシャルと、unfollowパーシャルを作成する。

app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

unfollowの方は、そのままform_forの引数にインスタンスを渡すと、patchリクエストになってしまうので、html: {method: :delete}としている。

三つのパーシャルができたので、follow_formパーシャルをusersのshowアクションのページに設置しよう。

app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

そしたら、relationshipsコントローラのcreateアクションとdestroyアクションのルーティングを設定しておこう。

config/routes.rb
Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
end

フォロワーやフォローしてる人一覧が出るページを作る。

そしたら、次はrelationshipsコントローラにdestroyアクションを定義していこう。
また、別々のアクションなのに、同じテンプレートを表示するように実装していく。

/users/id/followingと/users/id/followersで同じビューを使うってこと。

では、早速作っていこう

TDDで作っていくので、

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect following when not logged in" do
    get following_user_path(@user)
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get followers_user_path(@user)
    assert_redirected_to login_url
  end
end
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

まずは、共通の変数をもつ、followingアクションと、followersアクションを定義する。

次に、フォローしているユーザーとフォロワーの両方を表示するshow_followビューを定義する。
注意点としては、パーシャルではなく、usersリソースのビューであるという点だ。

これで、異なるアクションから同じビューを呼び出し、内容をごっそり変えるというテクニックができる。

app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

注目すべきは

<% if @users.any? %>
  <div class="user_avatars">
    <% @users.each do |user| %>
      <%= link_to gravatar_for(user, size: 30), user %>
    <% end %>
  </div>
<% end %>

これは、もしフォローしているユーザー、自分をフォローしているユーザが1人以上いれば、顔画像の集合体を表示しますよーってこと。

で、その下の、

<% if @users.any? %>
  <ul class="users follow">
    <%= render @users %>
  </ul>
  <%= will_paginate %>
<% end %>

これは、フォローしているユーザー、または自分をフォローしているユーザーがいればその集合体を表示するということ。

render @usersなので、eachメソッドを使って、_userパーシャルが展開される。

また、will_pagenateに関しては、@usersを指定しても良いが、usersのviewでuserリソースを扱うことがデフォルトで設定されているので、別に書かなくても良い。

次に、統合テストを作る。

$ rails generate integration_test following

まずはテスト用のサンプルデータを作っていく

test/fixtures/relationships.yml
one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

作ったサンプルデータを使って統合テストを書いていく

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end

これでテスト通る。

Relationshipsコントローラの実装。

フォローボタンを設置したはいいが、肝心のコントローラがまだなので、それを実装していく。

まずは、コントローラを作成する。

$ rails generate controller Relationships

次にrelationshipsコントローラの基本的なアクセス制御についてテストを書いていこう。

test/controllers/relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post relationships_path
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete relationship_path(relationships(:one))
    end
    assert_redirected_to login_url
  end
end

このテストを通るようにするために、各アクションとバリデーションを設定してく

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

次に二つのアクションの中身を実装していく。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

まずはcreateアクションから。
hidden_fieldタグで@user.idを格納したfollowed_idを送ったので、
params[:followed_id]と参照することができる。
ここにはプロフィールページの@userのidが入る。

次にdestroyアクションを見ていこう。

params[:id]のidはrelationshipのidカラムが入る。
なので、params[:id]をもつrelationshipにfollowedメソッドを実行することにより、
フォローされているユーザーが戻り、userという変数に格納される。

correct_userのバリデーションを設定しないのは?

それはcurrent_user.followにある。

もし、仮にいたずらでポストリクエストやデリートリクエストを送ったとする。

だとしても、フォローするのもフォローを解除するのもcurrent_userなので、誰かのアカウントを乗っ取ってフォローしたりフォローを解除しているわけではない。
他人と他人の関係性を勝手に作られるのなら困るけど、別に攻撃者のフォロワーやフォローしてる人が増え用が増えまいが関係ないよって話。

一応これでアクションの実装は完了。

発展編Ajaxを使ったフォローボタンの実装

これは、フォローする前とした後で、htmlにそこまで差がないことから、もう一度1から描画しなくても、変わった部分だけJS使って変えられないか?という話。

app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

まず、Ajaxを有効にするには、form_forの引数?オプションでremote: trueを有効にする。

そして、フォロー解除フォームでも同じことをする

app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

フォームの更新が終わったので、今度はこれに対応するRelationshipsコントローラを改造して、Ajaxリクエストに応答できるようにしましょう。こういったリクエストの種類によって応答を場合分けするときは、respond_toメソッドというメソッドを使います。

app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

これ何をやっているかというと、current_user.followでDBへの処理は終わってる。
その時点で、jsを使ってデータを再読み込みしてるらしい。

また、ユーザーの中にはjsが無効になっている人もいるので、無効だったらhtmlを使うように以下の処理をしておく

config/application.rb
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

次にjsと埋め込みRubyを使ってフォローの関係性を作成する。

app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

ここで何をやってるかというと、id = follow_formの部分を見つけて、以下(html("<%= escape_javascript(render('users/unfollow')) %>");)のhtmlに切り替えますよーっていう意味らしい。

これでAjaxの実装は終了。ただ、そこまで重要ではない。

Ajaxのテストをする

xhrオプションをtrueにすることで対応できるらしい。

test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user  = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
  .
  .
  .
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship), xhr: true
    end
  end
end

このままでテストは通る。

これでフォローボタン、アンフォローボタンのAjax版が終了

ステータスフィードを完成させよう

具体的には、フォローしてるユーザーの投稿が自分のタイムラインに出るということ。

で、答えがわかったので、まずはテストを書いていこう

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  .
  .
  .
  test "feed should have the right posts" do
    michael = users(:michael)
    archer  = users(:archer)
    lana    = users(:lana)
    # フォローしているユーザーの投稿を確認
    lana.microposts.each do |post_following|
      assert michael.feed.include?(post_following)
    end
    # 自分自身の投稿を確認
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end
    # フォローしていないユーザーの投稿を確認
    archer.microposts.each do |post_unfollowed|
      assert_not michael.feed.include?(post_unfollowed)
    end
  end
end

この状態ではfeedメソッドがプロトタイプなので落ちてしまう。

さて、これをどうやって実現するか?

current_user.microposts + current_user.following.map { |n| n.microposts }

これでも、自分と自分のフォローしている人の投稿を取得できるけど、発行されるSQL文が非常に多くなってしまう。

これをどうやったら簡潔に、そして1回のSQL文の発行で収めることができるか?

フィードを実装していく。

フィードはタイムラインのことだよ。

これはsqlに関係してて難しい。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

  # ユーザーをフォローする
  def follow(other_user)
    following << other_user
  end
  .
  .
  .
end

これは、一つ目の?にfollowing_ids
following_idsはhas_manyメソッドを定義すると同時に使えるようになるメソッド。
フォローしているユーザーのidの集合体を引っ張ってくる。

2つ目の?にはidが入る。self.idの略かな?

これでテストが通る。

サブセレクト

SQLの発行回数を減らして高速化しようねーって話。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
     following_ids: following_ids, user_id: id)
  end
  .
  .
  .
end

最終的な実装

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # ユーザーのステータスフィードを返す
  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end
  .
  .
  .
end

renderのurl指定について

renderのurlは基本的にはapp/views以下の相対パスを入れる。

例えば、render 'shared/stats'
html.erbは省略してもok

便利コマンド

$ rails test
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

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

RSpec導入、記述ルール

目的

新しいポートフォリオを作成する際に、RSpecを利用したTDD(テスト駆動開発)
に挑戦するため、RSpec導入します。ついでにRSpecによるテストの簡単な記述ルールについて説明したいと思います。

RSpecとは

RSpec とは Ruby プログラマー向けのBDD(Behaviour-Driven Development) ツールです。
BDD(Behaviour-Driven Development)は、
できることは普通のテストと同じで、加えて、
これから作成しようとするプログラムに期待される「振る舞い」や「制約条件」、
つまり「要求仕様」に近い形で、自然言語(英語)を併記しながらテストコードを記述することができるものです。

メリットは、テストコードの可読性があがる上、テストコードが要求仕様となりうることと、
要求仕様からテストコードを起こす際も、スムーズにコードに移行しやすいことです。

RSpecの導入

まずは RSpec の Gem パッケージをインストールします。

・・・
group :development, :test do
  gem 'rspec-rails', '~> 3.5'
end
・・・

ちなみに、RSpec は test フレームワークなのに、なぜインストールグループに development を追加するかというと、RSpec にはテストファイルを作成する generator があり、それを利用するために default の RAILS_ENV である development にインストールしておくと楽だからです。

インストールします。

$ bundle install

Gem パッケージがインストールされたら、次は Rails ソフトウェアに対して RSpec 用の初期ファイルをインストールする。

$ rails generate rspec:install
Running via Spring preloader in process 9045
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

テストファイルの記述ルール

テスト記述ルールとしては、テストする内容を説明する文章を引数として、describe, context, it を使って記述します。
1 つのテスト内容は it で記述し、期待する動作(expect)を 1 つ含めます。

require 'rails_helper'

RSpec.describe Diff, type: :model do
  it "is valid with diff string" do
    diff = Diff.new(diff: "some diff")
    expect(diff).to be_valid
  end
end

it に続く文字列は動詞から始める。

ここまでで 1つのテスト項目の記述ですが、実行すると、なぜitの次が動詞から始まる説明文を記述していたのか理由が分かります。

Diff
  is valid with diff string

Finished in 0.02296 seconds (files took 0.35495 seconds to load)
1 example, 0 failures

このように、describe, it で記述した内容がテスト結果に表示されるため、実行結果を見れば何をテストしているのかが分かるようになっています。

テストが特定の条件を想定する場合は context を使ってその条件を記述します。
ここで describe, context は入れ替えても動作しますし、以下の例では describe, context は記述せず it だけ記述しても動作しますが、テストが想定する内容が分かりやすくなるので積極的に使いましょう。

require 'rails_helper'

RSpec.describe Micropost, type: :model do
  describe "search posts by term" do
    context "when no post is found" do
      it "returns an empty collection" do
        expect(Micropost.search("John Doe")).to be_empty
      end
    end
  end
end

感想

RSpecの導入は思っていたより簡単でした。
RSpecテストの作成についてまだ知識が浅いため今回は初歩のところだけ説明しました。
知識がついたらテストの作成方法について詳しいブログを投稿します!!

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

はじめてのRuby on Rails環境構築(Mac)(2019/12/09)

はじめに

こんにちは。

ここでは、これまで主に、JavascriptやTypescriptでフロントを開発していたエンジニアが、ローカルのMacにRuby on Railsの開発環境を作ってみましたのでその手順をまとめてみました。

エンジニアスペック(主な開発経験)

Java
Python
Javascript
Typescript
React
ReactNative
Next
Node
Vue
など。

Homebrewのインストール

今回、購入直後の真新しいMacに環境構築を試みたので、まずはHomebrewのインストールから始めます。
https://brew.sh/index_ja

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

おっと、、エラーが出た。↓

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
==> This script will install:
/usr/local/bin/brew
/usr/local/share/doc/homebrew
/usr/local/share/man/man1/brew.1
/usr/local/share/zsh/site-functions/_brew
/usr/local/etc/bash_completion.d/brew
/usr/local/Homebrew

Press RETURN to continue or any other key to abort
You have not agreed to the Xcode license.
Before running the installer again please agree to the license by opening
Xcode.app or running:
    sudo xcodebuild -license

xcodeのライセンス承認をしろとのこと。
ライセンス承認後、再度上記コマンドを実行します。

成功!

Rubyのインストール

現時点でv2.6.5が使えそうなので、こちらを選択します。

$ brew install rbenv ruby-build
$ rbenv install --list
2.6.3
2.6.4
2.6.5
2.7.0-dev
2.7.0-preview1
2.7.0-preview2
2.7.0-preview3
$ rbenv install 2.6.5

pathを通します。

$ echo 'export PATH="~/.rbenv/shims:/usr/local/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

global環境を汚さないように、特定のディレクトリ下でv2.6.5を使えるようにします。

$ cd ./特定のディレクトリ
$ rbenv local 2.6.5

bundlerのインストール

次に、bundlerをインストールします。
gem でインストールするのですが、rbenv local したディレクトリで行います。

$ cd rbenv local 2.6.5 を実行したディレクトリ
$ gem install bundler
$ bundler -v
Bundler version 2.0.2

それ以外のディレクトリで bundler -v すると、

rbenv: bundler: command not found

The `bundler' command exists in these Ruby versions:
  2.6.5

のエラーが出ます。

Railsのインストール

まず、workspace 用のディレクトリを作ります。

mkdir workspace

Gemfile を生成します。下記コマンドを実行すると Gemfile が生成されます。

$ bundle init

Gemfile の下記1行のコメントアウトを削除します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails" ← ここ
# railsのインストール
$ bundle install --path=vendor/bundle

# railsのバージョンチェック
$ bundle exec rails -v
Rails 6.0.1

# workspace内にプロジェクトを作成
$ bundle exec rails new .

# 起動
$ bundle exec rails server
=> Booting Puma
=> Rails 6.0.1 application starting in development
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.1 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop

Yay! You’re on Rails!

最後に、 http://localhost:3000/ へアクセスしてページが表示されたら完了です!
Screen Shot 2019-12-09 at 19.43.26.png

ruby のバージョンが2.6.5になってました!
作業時間としてはおおよそ1h程度で、非常に簡単に環境が構築できました!

以上でRuby on Railsの開発環境ができましたので、いろいろ遊んでみようかと思います!

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

【Rails】エラーメッセージを使い回す

全てのフォームで、エラーメッセージを使いまわせるようにします。

関連:【Rails】バリデーションを実装する

リファクタリング前

以下のような書き方だと、@taskに対するエラーメッセージしか表示できないので、エラーメッセージ出力部分の記述をパーシャルに分けて使い回すことができません。

view
<h1>新規タスク</h1>
<% if @task.errors.any? %>
  <div>
    <ul style="color: red">
      <% @task.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>
<%= form_for(@task) do |f| %>
.
.

リファクタリング後

フォームの中に<%= render 'shared/error_messages', object: f.object %>と書き込み

view
<h1>新規タスク</h1>
<%= form_for(@task) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
・
・

パーシャルでobject.errorsを使ってエラーメッセージを呼び出します。

shared/error_messages.html.erb
<% if object.errors.any? %>
  <div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

注意

エラーメッセージが特定のActive Recordオブジェクトに関連付けられている場合のみ使える
→SessionはActive Recordオブジェクトに管理づけられていないので、ログインなどでは使えない
→flashで代用する必要がある

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

Active RecordをRubyで使うための方法

概要

Active RecordをRailsではなく、Rubyから利用する機会があったので、手順についてまとめておく。

$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580)

準備

今回はMySQLを利用することを前提とする。

MySQL2とActiveRecordの2種類のgemをインストールする。

gem install mysql2
gem install activerecord

データベースの準備

Mysqlで事前に、sampleデータベースを作成し、usesテーブルを作成しておく。
データベースや、テーブルの作成方法については、ここでは説明を省く。

利用

UserクラスでActiveRecordが利用できるようにする。

require "active_record"

ActiveRecord::Base.establish_connection(
    :adapter    =>"mysql2", 
    :host   =>"<ホスト名>",
    :database   =>"<db名>",
    :username   =>"<user名>",
    :password   =>"<パスワード>",
)

class User < ActiveRecord::Base
    def self.create_User
        User.create(name: "taro",age: 18) <= nameカラムとageカラムがある場合
    end
end

以上のような形で、usersテーブルに新たなレコードを追加することができる。当然createだけでなく、CRUD処理の全てを行うことができる。

まとめ

RailsからしかActiveRecordを利用したことがなかったが、Rubyからも簡単に利用することができた。

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

link_toメソッドの具体的な仕組み

Scaffoldでプロジェクトを作ると下記のようなリンクがindexなどに生成されるとおもうが、link_toメソッドの中に何があるのかいまいち理解せずに進めてしまっていた。

<% @products.each do |product| %>

----省略ーーー
<span><%= link_to 'Show', product %></span> 
<span><%= link_to 'Edit', edit_admin_product_path(product) %></span> 
<span><%= link_to 'Destroy', admin_product_path(product), method: :delete, data: { confirm: 'Are you sure?' } %></span>

Showは表示される名前であることはわかると思います。

しかしproductってなんだ??

多分eachメソッドのproducなんだろうけどproduct単体でいるのがよくわからない。。。

実はこれ省略されているんです!!
参考

<span><%= link_to 'Show', product_path(product) %></span>

これが省略せずに書いたものです。productはパラメーターとして渡されているんですね。
下記の流れをちゃんと把握しているとよく理解できると思います!

  • 画面からリンク押下してURLとHTTPメソッドがリクエストされる
    • それが ターミナルにログとして出る
  • コントローラーで想定したアクションが呼び出される
    • どのコントローラー、アクションが選択されたかログとして出る
    • アクションの途中で値が見たくなれば pやputs でできる
  • レスポンスして画面が表示される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Recordの関連付けに関する質問

railsチュートリアル2章のモデル間の関連付けについての質問です。

UserモデルとMicropostモデルの関連付けを行いました。

関係性
親 Userモデル
子 Micropostモデル
主キーはUserモデルのidで、外部キーがMicropostモデルのuser_idとなっています。

それぞれのモデルで以下のように宣言しています。

micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
end
user.rb
class User < ApplicationRecord
  has_many :microposts
end

Railsのconsoleを使って、ユーザーとマイクロポストの関連付けを確認しました。

$first_user = User.first
$first_user.microposts
$micropost = first_user.microposts.first
$micropost.user

チュートアルでは最終的にfirst_userのデータが表示されています。
しかし、エラーにはMicropostクラスでuserメソッドが定義されていないと出ていました。
ですが、以下のコードを打つと結果が上手く表示されました。

$first_micropost = Micropost.first
$first_micropost.user

userメソッドが定義されていない訳ではないと考えています。
二つのコードの違いは何なのでしょうか。
知識不足ですみません。教えていただけると嬉しいです。

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

railsでのデータの保存、更新、削除の方法 Create,Update,Delete

はじめに

今回はrailsでのデータの新規作成、更新、削除の方法をまとめていきます。

データの新規保存 

newメソッド+saveメソッドかcreateメソッドでレコードを新規作成します。

newメソッド+saveメソッドの例
#モデル名.new(カラム名: データ)+モデル名.saveのセットで保存されます。
#saveメソッド忘れに注意ください。これがないと保存されません。

#以下が例です。
post = Post.new(name: "記事1", title: "hogehoge") 
post.save
createメソッドの例
#モデル名.create(カラム名: データ)で保存されます。
#createメソッドはnewとsaveメソッドを一度に実行できます。

#以下が例です。
Post.create(name: "記事1", title: "hogehoge") 

データの更新

saveメソッドかupdateメソッドでレコードの更新を行います。
自身はupdateメソッドをよく使います。

updateメソッドの例
#モデル名.update(カラム名: データ)で保存されます。
#update前にfindでupdateをするデータを取得します。

#以下が例です。
post= Post.find(1)
post.update(name: "記事2", title: "higehige") 
saveメソッドの例
#モデルの各項目に値を入れていき、最後にsvaeメソッドで更新をかけます。

#以下が例です。
post= Post.find(1)
post.name = "記事2"
post.title = "higehige"
post.save

データの削除

destroyメソッドかdeleteメソッドを使いデータを削除します。

destroyメソッドの例(モデルクラスでdependent: :destroyを指定して関連付けたデータも自動的に削除されます)
#モデル名.find(削除したいid).destroy(カラム名: データ)でデータを削除します。

#以下が例です
Post.find(1).destroy
#ちなみにレコード全消去したい場合は以下になります。
Post.destroy_all
deleteメソッドの例(関連するデータは削除されません)
#モデル名.find(削除したいid).delete(カラム名: データ)でデータを削除します。

#以下が例です
Post.find(1).delete
#ちなみにレコード全消去したい場合は以下になります。
Post.delete_all

おわりに

基本的なrailsの新規作成、更新、削除について紹介いたしました。
データ新規登録時のsaveメソッドを忘れてはまらないように注意ください!!

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

Rubyでグラフを描画するツール GR.rb を作ったので紹介します

image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png image.png

これはなんの記事?

GRというグラフ描画ライブラリのRubyバインディングの記事です。

Rubyだってグラフを描きたいのです!

こんにちは。気がつくとRubyのコードをこちょこちょ書いているkojix2と申します。

 Rubyでグラフを描きたいって思ったことはありませんか? もちろんRubyにもグラフを描くツールはいくつかあります。例えばNArrayの作者の田中さんが作っているnumo-gnuplot、Jupyter-labで動かすiruby-plotly, Ankaneファミリーのchartkick、かつて一世を風靡し作者が忽然と姿を消したNyaplot、フロントエンドを目指すchartydaruと一緒に使うdaru-view。どれも良いツールではあるのですが、一長一短で私は満足できませんでした。

 2019年の初め、rubyplotの開発が発表されました。これはRubyにふさわしいAPIをもつグラフ描出用ライブラリということで、非常に期待していました。しかし開発者のみなさん忙しくてなかなか時間が取れないみたいで、開発は停滞して完成しなそうな雰囲気が漂ってきました。思えばRubyにふさわしいグラフ描画のためのAPIを一から考えて実装するのは簡単にできる仕事ではなかったのだと思います。

 ここ数ヶ月間、Ankaneさんが次々と機械学習のライブラリを発表されています。どうしてそんなことができるのかなと思って、プロジェクトの中身を観察してみると、ruby-ffiを利用していました。ffiを使えばC言語に詳しくなくてもバインディングを作成できるのではないかと思い、GR.rbの開発をはじめました。ネーミングは最初はffi-grだったのですが、文字が丸っこくて可愛いという理由でGR.rbを採用することにしました。

(GRではアニメーションの描出や、3Dの描出もできます! こういうのRubyではあんまり見ないでしょ?)

image.png gks.gif gks.gif gks.gif gks.gif gks.gif gks.gif gks.gif gks.gif gks.gif

インストール

GRの最新のリリース版をダウンロードして解凍し、適当なディレクトリに配置します。
環境変数GRDIRを設定して、GR.rbにgrの場所を教えます。

export GRDIR=/your/path/to/gr
gem install ruby-gr

簡単なつかい方

線グラフ

require 'gr/plot'

x = [0, 0.2, 0.4, 0.6, 0.8, 1.0]
y = [0.3, 0.5, 0.4, 0.2, 0.6, 0.7]

GR.plot(x, y)

Macでの実行画面
image.png

ヒストグラム

require 'numo/narray'
require 'gr/plot'

data = Numo::DFloat.new(10_000).rand_norm
GR.histogram(data)

Ubuntuでの実行画面
image.png

塗りつぶした2次元等高線図

require 'numo/narray'
require 'gr/plot'
include Numo

x = 8 * DFloat.new(100).rand - 4
y = 8 * DFloat.new(100).rand - 4
z = NMath.sin(x) + NMath.cos(y)
GR.contourf(x, y, z)

Windowsでの実行画面
image.png

タイトル、軸ラベルなど

require 'rdatasets'
require 'gr/plot'

passenger = RDatasets.datasets.AirPassengers
time = passenger.at(0).to_a
value = passenger.at(1).to_a

opts = { title: 'Air Passenger numbers from 1949 to 1961',
         ylabel: "Passenger numbers (1000's)",
         xlabel: 'Date'
       }
GR.plot(time, value, opts)

Windowsでの実行画面
image.png

さらに詳しい使い方を知りたい場合は、examplesディレクトリをご覧ください。スクリーンショットに取り上げられているグラフはいずれもExampleに入っています。(APIは今後も大きく変更になる可能性があります。)

GRの中には大きく2つのモジュールがあります。GRモジュールとGR3モジュールです。require 'gr' とすればGRモジュールが require 'gr3'とすればGR3モジュールが使えるようになります。これらは直接GRの関数を呼び出すものです。しかし、多くのケースではもっとお手軽にグラフを描出したいと思います。その場合は require 'gr/plot' をすると、GRモジュールに簡単にグラフを描出するメソッドが追加・上書きされるようになります。簡単にグラフを描画するメソッドの使用例は下記のExampleを見てください。

Jupyter

Jupyter + IRubyでも動作します。

image.png

そもそもGRってなに?

https://github.com/sciapp/gr
image.png

GRはユーリヒ総合研究機構Josef Heinenさん達が開発しているグラフ描画ライブラリです。GRはJuliaではPlotのデフォルトのバックエンドに採用されてます。PythonやR言語など特定の言語に依存しないグラフ描画ライブラリとしてはかなり有名な方じゃないかなと思います。Githubのコントリビューションを見ると、8年前から継続的な開発が続けられているのがわかります。
image.png

工夫したところ

Ankaneさんのプロジェクトのディレクトリ構成を参考にしました。ありがとうAnkaneさん。私がいろいろここに書くよりも、Ankaneさんのリポジトリを見るのが勉強になります。例えばlightgbmはコードの量も多くなくてオススメです。

具体的にはFFIモジュールを作って、そこにattach_functionでCの関数をRubyのメソッドとして登録します。Rubyといえばテストですが、Ankaneレポジトリを見ればffiを使うプロジェクトでtravis-ci等をどう設定すればいいかわかると思います。

私が工夫したところは、ブロック構文と継承を使って、RubyのArrayとpointerの相互変換を自動化したところです。(私はこれまでblock構文がよくわかっていなくて、はじめて満足がいく方法でブロックを使うことができた気がします。しかし、ひょっとするとこの書き方は、第三者が見た時にどうやって動作しているのかわかりにくくて、メンテナンス上はあまりよろしくないかもしれません。)

    def inqtext(x, y, string)
      inquiry [{ double: 4 }, { double: 4 }] do |tbx, tby|
        super(x, y, string, tbx, tby)
      end
    end

(同じメソッドがpython-grだとこうなる。Rubyの方がスッキリして見える気がする。)

def inqtext(x, y, string):
    tbx = (c_double * 4)()
    tby = (c_double * 4)()
    __gr.gr_inqtext(c_double(x), c_double(y), char(string), tbx, tby)
    return [[tbx[0], tbx[1], tbx[2], tbx[3]],
            [tby[0], tby[1], tby[2], tby[3]]]

それから、プログラミングに限った話ではありませんが、わからないことはいろいろと質問してみることも大事だなと感じました。英語版のstackoverflowやGithubのissue欄を使えば日本人だけでなく、外国の方に質問することができます。みらい翻訳で英文をこしらえてThanksと書くのを忘れなければ、たいていの場合親切に教えてくれます。

ffiでRubyからC言語の関数を呼ぶ

 ruby-ffiは非常に良くできていて、ほとんどの局面でうまく動作します。とくに今のruby-ffiはRubyInstaller2の方がメンテナンスしているので、Windowsの対応は抜群です。私はruby-ffiのことは結構好きです。けれども、ちょっと調べるとruby-ffiはかつてプロジェクトが死にかけた過去があるようです。ruby-ffiのリポジトリを眺めると、いくつかC言語のソースコードが含まれているのがわかります。私はC言語わかりませんが、Rubyの仕様が大きく変更になった時にruby-ffiを追従させていくのは結構な労力がいるんじゃないかなと推測します。

ruby-ffiからfiddleへの移行について

 Rubyからffiを使用するライブラリはruby-ffiのほかに、fiddleがあります。こちらはRubyチームが管理しているので、Rubyの仕様が変更になっても安定して更新されると期待されます。けれどもfiddleは仕様がシンプルすぎて、ユーザーが使うのは正直しんどくて、普及しないのも仕方ないという感じもします。

 今回はfiddleへの移行にあたりfiddleyというモジュールを使うことにしました。fiddleyはfiddleを使ってruby-ffiと同じ使用感を実現するツールです。でもfiddleyは未完成なので、実際にfiddleyを使うときは自分でいくつかコードを追加・改変していく必要がありました。

 そのほか、Windowsでfiddleを使う際に、パスを通すだけでは共有ライブラリ(dll)が読み込まれません。実はRubyInstaller.add_dll_directoryを呼ばなければならないという注意点があります。ruby-ffiではこのへんは自動でうまくやってくれているようです。詳しくは下記のリンクをご覧ください。

Windows の Ruby の fiddle で lib○○.dll が読み込めない時、何をチェックすればよいでしょうか?

GR.rbのこれから

red-data-toolsのプロジェクトになりました

 もともと使いやすいRubyのグラフ描画ライブラリが見当たらないこととから自分のために作りはじめたGR.rbですが、個人のプロジェクトなのでなかなかユーザーも増えず発展は難しいだろうなと思っていました。そんな時にred-data-toolsのプロジェクトにしないかとお声がかかり、いつもお世話になっているので参加することにしました。プロジェクトを更新する権限が自分にしかないと、不測の事態で自分がいなくなった時に、その時点でプロジェクトが終了してしまう可能性もあるので、それを防止するためにもいいかなと思いました。

issue報告求めています

ここまでGR.rbの報告をしてきました。たくさんスクリーンショットも貼り付けてきたのですが、実際には開発は始まったばかりで、たくさんのバグや実装上の課題が残っています。GR.rbのメンテナンスは、これから最低でも5年、できれば10年単位で継続していきたいなと思っていて、見つかったバグは少しずつ除去していきたいと思っています。なのでもしも不具合が見つかったら、GithubのGR.rbのページからissueやプルリクエストをして頂けると嬉しいなと思います。

2019年を振り返って

Rubyはオブジェクト指向言語であり、関数のかわりにメソッドが多用されます。そのため、データ処理には使いにくいという意見を持っておられる方も少なくないと思います。しかし今年一年間の、Rubyとデータ処理まわりを振り返ってみると、非常に多くの進歩がありました。

  • 機械学習ライブラリRumaleのブレイク
  • AnkaneさんによるPyTorchLightGBMのバインディングの公開
  • Numo NArrayが本格的に海外で知名度を獲得しはじめた
  • ApacheArrowの開発が進んだ

ほかにも、4日目のアドベントカレンダーのうなぎおおとろさんがRuby用のdeep learningライブラリruby-dnnを作ってくれたりして、地味ながらも着実な成長が感じられる1年だったと思います。

来年のみなさまの生活もきっと豊かなものでありますように。

この記事は以上です。

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

Railsコンソールで日本語入力ができない現象【Docker】

はじめに

Railsの参考書を読みながら勉強を進めているときに、

DockerのRailsコンテナ内で、Railsコンソールを起動し、日本語文章を入力しようとしたら、
入力はできるのに、エンターキーを押すとターミナルに表示されないという現象が起きたので、

その解決方法についてです。

エンターキーを押す前
image.png

押した後
image.png

解決方法

DockerFileに、

ENV LANG C.UTF-8

を記入して

docker-compose up --buildで再起動

これでコンソールに日本語を入力できるようになります。

エンターキーを押す前
image.png

押した後
image.png

参考にしたページ:Docker / rails console で日本語入力できない問題
https://gist.github.com/tasiyo7333/2163a09129ed36639645145a0146d8d3

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

discordrbで作る、1プロセスで複数BOTを動かす方法

この記事の対象

  • discordrbが何なのか程度はわかる人
  • Herokuの無料枠で複数のBOTを稼働させたい人

discordrbを使った基本的なBOTの作り方については他に記事があるため、この記事では割愛させていただきます。

作り方

さっそく本題になりますが、discordrb(というかRuby)ではPython(discord.py)のように特別マルチスレッドを意識しなくても簡単に1プロセスで複数のBOTを稼働させることができます。
以下がそのコードとなりますが、単にBOTごとにインスタンスを分けるだけで実現可能です。

require 'discordrb'

TOKEN1 = #1つ目のBOTのトークン
TOKEN2 = #2つ目のBOTのトークン

bot1 = Discordrb::Commands::CommandBot.new(token: TOKEN1, prefix: "/")
bot1.command(:test1) { "TEST BOT1!" }

bot2 = Discordrb::Commands::CommandBot.new(token: TOKEN2, prefix: "/")
bot2.command(:test2) { "TEST BOT2!" }

bot1.run(:async)
bot2.run

1つ注意が必要なのはDiscordrb::Bot#runはそのままだと以降の行の実行を止めてしまいます。
ですから、最後以外の#runでは引数に必ず:asyncを渡しておく必要があります。

動かす上での問題点

以上のようにインスタンスを分けるだけで複数のBOTを実行することができますが、残念ながら出力ログは分かてくれません。
どちらのBOTも同じようにログを出力するため、基本的に見分けることもできません。
よって実際の開発ではBOT別に十分デバッグを行った上で、運用段階で統合させるのが良いでしょう。
また、1つのアプリをデプロイするために、全てのアプリを起動しなおすことになるため、わりと手間がかかります。

開発するときの工夫

実際の開発では上の例のように複数のBOTを1つのコード内に書くことはありませんし、するべきではありません。
そのため、BOTごとにクラスを定義して実行用のスクリプトで呼び出す方が現実的かと思います。
以下は私が実際に運用しているBOTで使用している方法です。

test_bot1.rb
require 'discordrb'

class TestBot1
  def initialize(token)
    @bot = Discordrb::Commands::CommandBot.new(token: token, prefix: "/")

    @bot.ready { @bot.game = "TestBot1" }

    @bot.command(:test1) { "TestBot1" }
  end

  def run(async = false)
    @bot.run(async)
  end
end
test_bot2.rb
require 'discordrb'

class TestBot2
  def initialize(token)
    @bot = Discordrb::Commands::CommandBot.new(token: token, prefix: "/")

    @bot.ready { @bot.game = "TestBot2" }

    @bot.command(:test2) { "TestBot2" }
  end

  def run(async = false)
    @bot.run(async)
  end
end
execute_bots.rb
require './test_bot1'
require './test_bot2'

test_bot1 = TestBot1.new("BOTのトークン")
test_bot2 = TestBot2.new("BOTのトークン")

test_bot1.run(:async)
test_bot2.run

最後に

初めてAdvent Calendarというものに参加させていただきました。
優しい日本語を心がけようとした結果、少しおかしな読みにくい文章になってしまいましたが、最後まで読んで頂きありがとうございます。
何かあればお気軽にコメントを頂ければと思います。
それでは。

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

Rubyとseleniumでブラウザを操作してCVテストを自動化してみた

CVテストの半自動化を試みた

同じ入力項目のLPが何十個もあるサイトで大量のCVテストが辛いのでRubyとseleniumを使って半自動化してみました。正常系だけでも半自動で確認できたらだいぶ楽かと思います。スクショも自動で取るように設定できるので勝手にエビデンスも残してくれます。

seleniumのインストール

Rubyがインストール済だったらseleniumのインストールは下記コマンドでOKです

gem install selenium-webdriver

ChromeDriver のインストール

which rubyでルビーがインストールされている場所を確認します。

$ which ruby
/c/Ruby24-x64/bin/ruby

ChromeDriverを https://chromedriver.chromium.org/downloads よりダウンロードします。
ChromeDriverのバージョンがchromeと合ってないと正常に動作しないので注意が必要です。
ダウンロードしたファイル(chromedriver.exe)which rubyで確認できたディレクトリに置きます。

サンプルコード

test.rb
require 'selenium-webdriver'

driver = Selenium::WebDriver.for :chrome
driver.navigate.to "https://sample.com"

### 性別
unless driver.find_element(:name, 'gender').selected?
  query = driver.find_element(:name, 'gender').click
end

### 生年月日
# 年
query = Selenium::WebDriver::Support::Select.new(driver.find_element(:name, 'year'))
query.select_by(:value, '1990')
# 月
query = Selenium::WebDriver::Support::Select.new(driver.find_element(:name, 'month'))
query.select_by(:value, '01')
# 日
query = Selenium::WebDriver::Support::Select.new(driver.find_element(:name, 'day'))
query.select_by(:value, '01')

### 職業
query = Selenium::WebDriver::Support::Select.new(driver.find_element(:name, 'career'))
query.select_by(:value, 'その他')

# 途中経過をスクリーンショットで取得
driver.save_screenshot('step1.png')
sleep 1

### お名前
query = driver.find_element(:name, 'name_sei')
query.send_keys('山田太郎')

### フリガナ
query = driver.find_element(:name, 'ruby_sei')
query.send_keys('ヤマダタロウ')

### 電話番号
query = driver.find_element(:name, 'phone')
query.send_keys('09039858495')

### メールアドレス
query = driver.find_element(:name, 'email')
query.send_keys('sample@example.com')

### 郵便番号
query = driver.find_element(:name, 'zip')
query.send_keys('1410022')
driver.execute_script("console.log('住所取得')")
sleep 1

### 利用規約
unless driver.find_element(:name, 'agree').selected?
  query = driver.find_element(:name, 'agree').click
end

# 途中経過をスクリーンショットで取得
driver.save_screenshot('step2.png')

# 送信ボタンをクリック(送信)
driver.find_element(:id, "js-submit").click

# CV後のページをスクリーンショットで取得
driver.save_screenshot('step3.png')

サンプルコード説明

selenium-webdriverの読み込み

require 'selenium-webdriver'

chromeを起動

driver = Selenium::WebDriver.for :chrome
#chromeの部分をfirefox ie opera 等に設定することも可能です。

起動時のURLを指定

driver.navigate.to "https://sample.com"

チェックボックス・ラジオボタンを選択

unless driver.find_element(:name, 'gender').selected?
  query = driver.find_element(:name, 'gender').click
end

セレクトタグをoptionのvalueより選択

query = Selenium::WebDriver::Support::Select.new(driver.find_element(:name, 'year'))
query.select_by(:value, '1990')

スクリーンショットを取得

driver.save_screenshot('step1.png')

nameで取得したテキストエリアに値を入力

下記の場合nameは「name_sei」で入力する値は「山田太郎」となります。

query = driver.find_element(:name, 'name_sei')
query.send_keys('山田太郎')

JavaScriptを実行

driver.execute_script("console.log('住所取得')")

指定した要素をクリック

driver.find_element(:id, "js-submit").click

テスト実行

以下のコマンドでテストが実行されます。

cd /path to test.rb
ruby test.rb

使ってみた感想

実際使ってみて、自動でCVテストが出来るのはちょっとした感動を覚えました。
人がCVテストする前提で補助ツールとして導入したらかなり強力だと思います。

ただし、CVテストの自動化はどのサイトでもできるような万能なものではないと感じました。例えば、入力項目がある程度固定されているフォームだったら導入後の確認がすごく楽になりそうですが、頻繁に仕様が変わるフォームではテストプログラムのメンテナンスが大変になります。

また、PCとSPで実装方法が統一されていないと当然テスト用のプログラムも2つ書く必要があります。JSで動的に切り替わるフォームについても実装は(可能ですが)結構キツイように思います。

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

Airtable ruby クライアント airtable を使う

概要

  • Airtableは、excel様の表計算の使用感で、入力型の指定、添付ファイル、1対多のデータを取り扱えるサービスです。
  • AirtableはデータにアクセスできるAPIを用意しています。
  • 今回は、APIをrubyで使いやすくデータを読み込み書き込みできるAPIクライアントgem airtableを使ってみました。

インストール

railsであれば、下記の一文を足して bundle install です。

gem 'airtable'

railsでない場合は、activesupportが必要なので下記のようにしてください。

gem install activesupport
gem install airtable
# 使いたいところでrequireする
require 'active_support/all' # https://github.com/Airtable/airtable-ruby/issues/5
require 'airtable'

使い方

下記のようなデータ(Airtableではbaseと表現されるの以下baseと表現します。)があると仮定します。

  • eventsテーブル。 image.png

eventsテーブルとpalcesが1対1。eventsテーブルとtagsテーブルが1対多の関係にあります。

前準備

baseにアクセスするには、アカウント自身のapi keyと base自身のapi idが必要です。下記からコピーしてください。

基本的な操作

  1. アカウント自身のapi key を使ってAirtableにアクセスするclientを作る
  2. clientをに対して、 base自身のapi idtable名 を使ってtableを指定
  3. tableに対してデータ読み書き操作を実行する

例えば、FESTIVAL baseの eventsテーブルの全データを取得する場合は以下の通りです。

tableを指定

require 'active_support/all' # airtableがActiveSupportをつかっているので必要
require 'airtable'
api_key = 'アカウント自身のapi key' # アカウント毎に1つ
app_token = 'base自身のapi id' # FESTIVAL base

client = Airtable::Client.new(api_key)
table = client.table(api_token, 'events') # events tableを指定する

table.all で全データを読み取る

records = table.all # events tableを全部呼び出す
p records
[
#<Airtable::Record :id=>"rec4jTsyTBJ2GEG9D">,
 #<Airtable::Record :place=>["recUZQa12QxnpYlD4"], :start=>"2020-02-19", :tags=>["recynBCi1UsxNakr6"], :attachments=>[{"id"=>"attOUbz2csayVdwgf", "url"=>"https://dl.airtable.com/.attachments/534ea5f6b38dbd51cbd168e71dce36b6/2ec2b44f/thumbnail_book_smile.jpg", "filename"=>"thumbnail_book_smile.jpg", "size"=>13155, "type"=>"image/jpeg", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f0a901d1f23fe8150e67f8d6e897e641/86ac3878", "width"=>36, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/a2efaf458b5779af1edc3afe5eb5c4ae/f2bb971a", "width"=>180, "height"=>180}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f9899e2b7b132282ac7028f363c8956c/5861b6c7", "width"=>3000, "height"=>3000}}}], :notes=>"夏目漱石のこころを朗読するよ\n", :name=>"朗読フェスティバル", :id=>"recLaCiRU8AAGm0PF">,
 #<Airtable::Record :place=>["recUZQa12QxnpYlD4"], :start=>"2019-10-22", :tags=>["recpnIdd9uMlegCLZ"], :attachments=>[{"id"=>"attCLeFeSWWViub0n", "url"=>"https://dl.airtable.com/.attachments/ed89fb1a36074958b3e97b8288b4eb06/13ddd3b9/rapper.png", "filename"=>"rapper.png", "size"=>31094, "type"=>"image/png", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/22b1e4ab19464c76bd14d2714d9c62e6/e88cc241", "width"=>36, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/7added43e6cd8c130a4674c4dd7f597d/1a7682f1", "width"=>180, "height"=>180}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/9d13f0b73a6b82a6339c81d06f0afa96/23df6c2a", "width"=>3000, "height"=>3000}}}], :notes=>"エリザベートよ", :name=>"演劇フェスティバル", :id=>"recMrGMLpV8orHwK2">,
 #<Airtable::Record :place=>["recrbd8K65GT5bFJT"], :start=>"2019-10-15", :tags=>["recx6N45MwnImnIqi", "recynBCi1UsxNakr6"], :attachments=>[{"id"=>"attNoJgJSivCZxZ6b", "url"=>"https://dl.airtable.com/.attachments/eba3a91c1e8484e604d8709b6a39e610/7dc48cb0/engeki.png", "filename"=>"engeki.png", "size"=>287149, "type"=>"image/png", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/ee3d568f09b1bdf1116a2a58cb8c614e/3d722f18", "width"=>44, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/0d4fb6ce5e22dc5a885327099c378347/70ee925c", "width"=>500, "height"=>408}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/5f99a30d260d640b6b31c69dabeea3da/e517f559", "width"=>3000, "height"=>3000}}}], :notes=>"吹奏楽の演奏があるよ", :name=>"音楽フェスティバル", :id=>"recbAn2S44M4IAzEP">
]

tableに新規レコードを書き込む

record = Airtable::Record.new(name: 'hoge festival', notes: "note\nmemo")
table.create(event)

新規レコードが書き込まれました。

Image from Gyazo

関連レコードを参照する

Airtableも魅力のひとつは関連レコードを容易に書き込めることです。
一手間かかりますが、書き込まれた関連レコードもAPIをつかうことで参照することができます。

事例は、eventのタグ付けように用意したtagsテーブルのデータを読み込む方法です。

  • eventsテーブル。
    image.png

  • tagsテーブル
    image.png

まず、eventのデータを引っ張ります。

event = table.select(formula: 'name = "音楽フェスティバル"').first

こちらのデータのtagsデータ見てみます。

event.tags
=> ["recx6N45MwnImnIqi", "recynBCi1UsxNakr6"]

tagsデータにはidが入っています。
このidを使って、こちらを使って関連データ(tagsのデータ)を引っ張ることができます。
idを元にしてデータを持ってくるには find methodを使います。(なんだかrailsっぽくてわかりやすいですね!)

irb(main):039:0> table.find('recx6N45MwnImnIqi')
=> #<Airtable::Record :name=>"食べ物可", :events=>["recbAn2S44M4IAzEP"], :id=>"recx6N45MwnImnIqi">
irb(main):040:0> table.find('recynBCi1UsxNakr6')
=> #<Airtable::Record :name=>"ペット可", :events=>["recbAn2S44M4IAzEP", "recLaCiRU8AAGm0PF"], :id=>"recynBCi1UsxNakr6">
irb(main):041:0> 

以上。

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

Airtable API ruby クライアント airtable を使う

概要

  • Airtableは、excel様の表計算の使用感で、入力型の指定、添付ファイル、1対多のデータを取り扱えるサービスです。
  • AirtableはデータにアクセスできるAPIを用意しています。
  • 今回は、APIをrubyで使いやすくデータを読み込み書き込みできるAPIクライアントgem airtableを使ってみました。

インストール

railsであれば、下記の一文を足して bundle install です。

gem 'airtable'

railsでない場合は、activesupportが必要なので下記のようにしてください。

gem install activesupport
gem install airtable
# 使いたいところでrequireする
require 'active_support/all' # https://github.com/Airtable/airtable-ruby/issues/5
require 'airtable'

使い方

下記のようなデータ(Airtableではbaseと表現されるの以下baseと表現します。)があると仮定します。

  • eventsテーブル。 image.png

eventsテーブルとpalcesが1対1。eventsテーブルとtagsテーブルが1対多の関係にあります。

前準備

baseにアクセスするには、アカウント自身のapi keyと base自身のapi idが必要です。下記からコピーしてください。

基本的な操作

  1. アカウント自身のapi key を使ってAirtableにアクセスするclientを作る
  2. clientをに対して、 base自身のapi idtable名 を使ってtableを指定
  3. tableに対してデータ読み書き操作を実行する

例えば、FESTIVAL baseの eventsテーブルの全データを取得する場合は以下の通りです。

tableを指定

require 'active_support/all' # airtableがActiveSupportをつかっているので必要
require 'airtable'
api_key = 'アカウント自身のapi key' # アカウント毎に1つ
app_token = 'base自身のapi id' # FESTIVAL base

client = Airtable::Client.new(api_key)
table = client.table(api_token, 'events') # events tableを指定する

table.all で全データを読み取る

records = table.all # events tableを全部呼び出す
p records
[
#<Airtable::Record :id=>"rec4jTsyTBJ2GEG9D">,
 #<Airtable::Record :place=>["recUZQa12QxnpYlD4"], :start=>"2020-02-19", :tags=>["recynBCi1UsxNakr6"], :attachments=>[{"id"=>"attOUbz2csayVdwgf", "url"=>"https://dl.airtable.com/.attachments/534ea5f6b38dbd51cbd168e71dce36b6/2ec2b44f/thumbnail_book_smile.jpg", "filename"=>"thumbnail_book_smile.jpg", "size"=>13155, "type"=>"image/jpeg", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f0a901d1f23fe8150e67f8d6e897e641/86ac3878", "width"=>36, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/a2efaf458b5779af1edc3afe5eb5c4ae/f2bb971a", "width"=>180, "height"=>180}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f9899e2b7b132282ac7028f363c8956c/5861b6c7", "width"=>3000, "height"=>3000}}}], :notes=>"夏目漱石のこころを朗読するよ\n", :name=>"朗読フェスティバル", :id=>"recLaCiRU8AAGm0PF">,
 #<Airtable::Record :place=>["recUZQa12QxnpYlD4"], :start=>"2019-10-22", :tags=>["recpnIdd9uMlegCLZ"], :attachments=>[{"id"=>"attCLeFeSWWViub0n", "url"=>"https://dl.airtable.com/.attachments/ed89fb1a36074958b3e97b8288b4eb06/13ddd3b9/rapper.png", "filename"=>"rapper.png", "size"=>31094, "type"=>"image/png", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/22b1e4ab19464c76bd14d2714d9c62e6/e88cc241", "width"=>36, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/7added43e6cd8c130a4674c4dd7f597d/1a7682f1", "width"=>180, "height"=>180}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/9d13f0b73a6b82a6339c81d06f0afa96/23df6c2a", "width"=>3000, "height"=>3000}}}], :notes=>"エリザベートよ", :name=>"演劇フェスティバル", :id=>"recMrGMLpV8orHwK2">,
 #<Airtable::Record :place=>["recrbd8K65GT5bFJT"], :start=>"2019-10-15", :tags=>["recx6N45MwnImnIqi", "recynBCi1UsxNakr6"], :attachments=>[{"id"=>"attNoJgJSivCZxZ6b", "url"=>"https://dl.airtable.com/.attachments/eba3a91c1e8484e604d8709b6a39e610/7dc48cb0/engeki.png", "filename"=>"engeki.png", "size"=>287149, "type"=>"image/png", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/ee3d568f09b1bdf1116a2a58cb8c614e/3d722f18", "width"=>44, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/0d4fb6ce5e22dc5a885327099c378347/70ee925c", "width"=>500, "height"=>408}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/5f99a30d260d640b6b31c69dabeea3da/e517f559", "width"=>3000, "height"=>3000}}}], :notes=>"吹奏楽の演奏があるよ", :name=>"音楽フェスティバル", :id=>"recbAn2S44M4IAzEP">
]

tableに新規レコードを書き込む

record = Airtable::Record.new(name: 'hoge festival', notes: "note\nmemo")
table.create(event)

新規レコードが書き込まれました。

Image from Gyazo

関連レコードを参照する

Airtableも魅力のひとつは関連レコードを容易に書き込めることです。
一手間かかりますが、書き込まれた関連レコードもAPIをつかうことで参照することができます。

事例は、eventのタグ付けように用意したtagsテーブルのデータを読み込む方法です。

  • eventsテーブル。
    image.png

  • tagsテーブル
    image.png

まず、eventのデータを引っ張ります。

event = table.select(formula: 'name = "音楽フェスティバル"').first

こちらのデータのtagsデータ見てみます。

event.tags
=> ["recx6N45MwnImnIqi", "recynBCi1UsxNakr6"]

tagsデータにはidが入っています。
このidを使って、こちらを使って関連データ(tagsのデータ)を引っ張ることができます。
idを元にしてデータを持ってくるには find methodを使います。(なんだかrailsっぽくてわかりやすいですね!)

irb(main):039:0> table.find('recx6N45MwnImnIqi')
=> #<Airtable::Record :name=>"食べ物可", :events=>["recbAn2S44M4IAzEP"], :id=>"recx6N45MwnImnIqi">
irb(main):040:0> table.find('recynBCi1UsxNakr6')
=> #<Airtable::Record :name=>"ペット可", :events=>["recbAn2S44M4IAzEP", "recLaCiRU8AAGm0PF"], :id=>"recynBCi1UsxNakr6">
irb(main):041:0> 

添付ファイルにアクセスする

添付ファイルにも簡単にアクセスできます。
eventsテーブルは attachementsとう名称で添付ファイル(画像とか)を管理しています。
こちらも下記のようにアクセスできます。

event.attachements

=> [{"id"=>"attOUbz2csayVdwgf", "url"=>"https://dl.airtable.com/.attachments/534ea5f6b38dbd51cbd168e71dce36b6/2ec2b44f/thumbnail_book_smile.jpg", "filename"=>"thumbnail_book_smile.jpg", "size"=>13155, "type"=>"image/jpeg", "thumbnails"=>{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f0a901d1f23fe8150e67f8d6e897e641/86ac3878", "width"=>36, "height"=>36}, "large"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/a2efaf458b5779af1edc3afe5eb5c4ae/f2bb971a", "width"=>180, "height"=>180}, "full"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f9899e2b7b132282ac7028f363c8956c/5861b6c7", "width"=>3000, "height"=>3000}}}]

上記配列、ハッシュの中身を見てもらえればわかるかと思います。
意外に便利だなーと思ったのは、thumbnailアップロードするだけで大中小のサムネイル画像も作ってもらえるので、そのまま流用すれば、サムネイル作る手間が省けます!!

"thumbnails"=>
{"small"=>{"url"=>"https://dl.airtable.com/.attachmentThumbnails/f0a901d1f23fe8150e67f8d6e897e641/86ac3878", 
"width"=>36, 
"height"=>36}, 
# 以下large, fullが続く。

以上です。
Airtableもっと流行るといいなー。

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

2億件の本番データを無事に変換した話

この記事はクラウドワークス Advent Calendar 2019 の11日目の記事です。

昨日は@lemtosh469による「アジャイル開発のスケーリングの方法」でした。

はじめに

みなさんこんにちは、クラウドワークスでサーバーサイドエンジニアをしている@shimopataです。普段はRailsを中心に機能開発したり改修作業とかに勤しんでいます。
先日、大量のデータ変換を伴うテーブルの差し替え作業をしました。データ量も多く、大変な作業でしたが計画、検証からやらせてもらい貴重な経験だったので、公開できる範囲で共有させて頂こうと思います。

やりたいこと

あるテーブルに保存されているデータの形式を変換し保存し直す。
対象は以下のテーブル

テーブル名 レコード数
テーブルA 65,000件
テーブルB 4,300,000件
テーブルC 200,000,000件

2億件もレコードあるのかー:innocent:

やったこと

調査と方針決め

まずはじめに、対象のテーブルがどのように使用されているのかについて調べました。一度登録されたデータがユーザの操作等によって更新されるかどうかで、対応方針が大きく異なると思い、調査し始めました。
結果、一度登録されたデータはユーザーによって、変更・削除されないことが分かり、以下の計画で変換することにしました。

【計画】
1. 変換後の値を保存するテーブルを作成
2. データを変換するスクリプトを実行し、1で作成したテーブルに保存
3. テーブル名をリネームして差し替える
4. 不要なモデル、テーブルを削除

図に示すと次のような感じです。
データ変換の図 (1).png

メリットとしては次の通り、安心安全にデータの変換が行えるのでこの方式を採用しました。

  • 書き込みを行うテーブルはユーザが直接触るテーブルではないため、書き込み時にテーブルをロックしてもユーザ影響はなく、日中帯に作業ができる。
  • 変換前のデータがテーブルに残るので何かあっても、そこからリカバリーができる。

スクリプトの作成について

データ件数が多く、処理時間が長くなってしまうことから、少しでも短くするために以下の対応を行いました。

find_in_batchesを使用して、変換速度の底上げ

繰り返し処理をする場合、データ数が少なければ、eachなどを事足りるのですが、今回は、データ数が多いのでfind_in_batchesを使用しました。find_eachとの違いは

  • find_eachはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し、一件ずつをeachで処理していく
  • find_in_batchesはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し,1配列毎に処理していく

どのくらい違いが出るかを手元にあるテストデータで試してみます。

find_each
Benchmark.realtime do
  User.find_each(batch_size: 1000) { |user| p user }
end

-------以下、実行結果-----------

<User id: 1, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">
<User id: 2, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">
...
<User id: 2000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">
=> 1.006721000012476
find_in_batches
Benchmark.realtime do
  User.find_in_batches(batch_size: 1000) { |user| p user }
end

-------以下、実行結果-----------

[<User id: 1, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">,
・・・
<User id: 1000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">]

[<User id: 1001, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">,
・・・
<User id: 2000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">]
=> 0.4186677999678068

2000件程度のデータで約二倍の速度の違いが出ていますね。対象となるデータはこの1000000倍なので、find_in_batchesを使うしかないですね。

また、バッチサイズのサイジングもこのスクリプトのキーポイントでした。この数値を大きくする事で一度に処理できるデータの数を増やすことができ、その分、速度が早くなるのですが、当然、メモリを多く使用してしまいます。安全策をとるのであれば、夜に少しづつ変換を進めていくこともできたのですが、作業締め切りもあるため日中帯にスクリプトを流す必要がありました。

速度を出すために、バッチサイズを大きくしたい、でも、サービスに負荷をかけれない。。。

そのため、検証環境に大量のダミーデータを用意し、検証を行いました。バッチサイズの大きさを変更し実行しては、AWSコンソールメトリクスを確認を繰り返し、最適値を探しました。速度面、リソースの安全性からバッチサイズは5000としました。

activerecord-importを使用して書き込み速度の底上げ

activerecord-importというgemを使用しました。これは、一括でINSERT処理やUPDATE処理を行えるようにするgemで、スクリプト実行時間の大幅な短縮をすることができました。
使い方としては次のような感じです。

sample.rb
books = []
10.times do |i|
  books << Book.new(name: "book #{i}")
end
Book.import books 

modelにimportというメソッドが追加されるので、引数に取り込みたいデータを入れてあげるだけで、簡単にINSERT処理が行えます。また、オプションもたくさんあるため(特定のカラムだけ取り込む、取り込み時にバリデーションを張るなど)、興味があったら、gemのREADMEをみてみてください。

こちらも、どのくらい違いが出るかを手元にあるテストデータで試してみます。

sample_bulk_import.rb
users = []
Benchmark.realtime do
  1000.times do |i|
    users << TestUser.new(id: i,name: "test")
  end
  TestUser.import users
end

------------以下実行結果-----------------
0.1599934000405483

sample_import.rb
Benchmark.realtime do
  1000.times do |i|
    TestUser.create!(name: "test")
  end
end

------------以下実行結果-----------------
5.3943878000136465

圧倒的に早そうですね。

変換スクリプトの実施

事前検証も終え、負荷なども問題ないことを確認した上で、本番サーバーに対して変換作業を行いました。いくら使用していないテーブルとはいえ、本番環境に対する変更のため、二週の間、対象となるデータを約1000万件単位に分割し勤務時間帯にのみスクリプトを実行していたのですが、とにかくデータ量が多く、時間がかかりました。リソースの監視はAWSのマネジメントコンソールの情報を中心に確認していました。

image.png

いつ、どのような不具合が発生するのか分からないという状況で長時間作業していくのはなかなか体力と精神を削られる作業で大変でした。:cold_sweat:

テーブルの差し替え

変換作業が完了後、テーブル名を変換するmigrationを作成し、テーブルの差し替えを行いました。

xxxx_rename_table.rb
def up
 rename_table :target_table, :backup_target_table
 rename_table :converted_table, :target_table
end

作業を初めて見ると、なかなかmigrationが終わらず、原因を探るために、migration実行中に発行しているクエリを確認しました。
次のクエリを実行して見てみると、インデックスの再作成で時間かかっている事が分かりました。
SELECT * FROM information_schema.PROCESSLIST WHERE Time >= 10 AND command = 'Query' ORDER BY Time DESC;

クエリ自体は問題なく発行されているので、ただ待つしかなく、結果として、作業時間を超えてしまったのですが、大量のデータ取り扱ってく上での学びになりました。

終わりに

テーブルの差し替え作業のタイミングでチームを移ってしまい、先輩エンジニアに作業を引き継ぎました。この作業のタイミングで、tableBのデータについて、特定の条件下で登録したデータがユーザによって削除であることが分かりました。1が、なんとかこれも対応してもらい、ほんと、ありがとうございました。

データ変換をやり始めてからでないと分からないリスクとして、

  • バリデーションなどの制約から外れているデータ
  • 過去に何らかの理由で手動でデータ修正をして、想定していない形で入っているデータ

などを想定していたのですが、実際にやってみると意外に該当するものは少なく、よかったです。

振り返ってみるとやっていること自体は単純だったりするのですが、やっていた当時は「本当に大丈夫なんだろうか、問題ないのだろうか」とリスクがないことを証明する悪魔の証明をずっとしながら、作業工程を組み立てていたので、非常に不安になりながら作業を進めていました。
そんな状況の中でも、相談に乗ってくれたり、作業に協力してくれたエンジニアに支えられてなんとかゴールまで行けたのかなと思います。

ゴールに辿り着くまでに色々失敗をしたりしたのですが、学びとして得られるものも大きく、今後に生かしていきたいと思います。


  1. 最初の調査の時に気づけなかったのは反省です。。。 

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

rbenvを使ってruby2.6.0を入れたいのにリストに表示されない時の対処法のメモ

環境

$ rbenv -v
rbenv 1.1.2

現象

rbenvを使ってruby2.6.0を入れたいが下記コマンドを使うと2.6.0が無いと表示される

rbenv install 2.6.5

エラー内容

ruby-build: definition not found: 2.6.5

See all available versions with `rbenv install --list'.

If the version you need is missing, try upgrading ruby-build:

rbenv install --listをつかえば、利用可能なバージョン一覽が出てくるということでコマンドを使ってみた

コマンド

rbenv install --list

表示された内容

確かに2.6.5が存在しない
rbenvが最新じゃないのかなと思って調べたが1.1.2で最新だった

Available versions:
  1.8.5-p52
  1.8.5-p113
  1.8.5-p114
  1.8.5-p115
  1.8.5-p231
  1.8.6
  1.8.6-p36
  1.8.6-p110
  1.8.6-p111
  1.8.6-p114
  1.8.6-p230
  1.8.6-p286
  1.8.6-p287
  1.8.6-p368
  1.8.6-p369
  1.8.6-p383
  1.8.6-p388
  1.8.6-p398
  1.8.6-p399
  1.8.6-p420
  1.8.7-preview1
  1.8.7-preview2
  1.8.7-preview3
  1.8.7-preview4
  1.8.7
  1.8.7-p17
  1.8.7-p22
  1.8.7-p71
  1.8.7-p72
  1.8.7-p160
  1.8.7-p173
  1.8.7-p174
  1.8.7-p248
  1.8.7-p249
  1.8.7-p299
  1.8.7-p301
  1.8.7-p302
  1.8.7-p330
  1.8.7-p334
  1.8.7-p352
  1.8.7-p357
  1.8.7-p358
  1.8.7-p370
  1.8.7-p371
  1.8.7-p373
  1.8.7-p374
  1.8.7-p375
  1.9.0-0
  1.9.0-1
  1.9.0-2
  1.9.0-3
  1.9.0-4
  1.9.0-5
  1.9.1-preview1
  1.9.1-preview2
  1.9.1-rc1
  1.9.1-rc2
  1.9.1-p0
  1.9.1-p129
  1.9.1-p243
  1.9.1-p376
  1.9.1-p378
  1.9.1-p429
  1.9.1-p430
  1.9.1-p431
  1.9.2-preview1
  1.9.2-preview3
  1.9.2-rc1
  1.9.2-rc2
  1.9.2-p0
  1.9.2-p136
  1.9.2-p180
  1.9.2-p290
  1.9.2-p318
  1.9.2-p320
  1.9.2-p326
  1.9.2-p330
  1.9.3-dev
  1.9.3-preview1
  1.9.3-rc1
  1.9.3-p0
  1.9.3-p105
  1.9.3-p125
  1.9.3-p194
  1.9.3-p286
  1.9.3-p327
  1.9.3-p362
  1.9.3-p374
  1.9.3-p385
  1.9.3-p392
  1.9.3-p426
  1.9.3-p429
  1.9.3-p448
  1.9.3-p484
  1.9.3-p545
  1.9.3-p547
  1.9.3-p550
  1.9.3-p551
  2.0.0-dev
  2.0.0-preview1
  2.0.0-preview2
  2.0.0-rc1
  2.0.0-rc2
  2.0.0-p0
  2.0.0-p195
  2.0.0-p247
  2.0.0-p353
  2.0.0-p451
  2.0.0-p481
  2.0.0-p576
  2.0.0-p594
  2.0.0-p598
  2.0.0-p643
  2.0.0-p645
  2.0.0-p647
  2.0.0-p648
  2.1.0-dev
  2.1.0-preview1
  2.1.0-preview2
  2.1.0-rc1
  2.1.0
  2.1.1
  2.1.2
  2.1.3
  2.1.4
  2.1.5
  2.1.6
  2.1.7
  2.1.8
  2.1.9
  2.1.10
  2.2.0-dev
  2.2.0-preview1
  2.2.0-preview2
  2.2.0-rc1
  2.2.0
  2.2.1
  2.2.2
  2.2.3
  2.2.4
  2.2.5
  2.2.6
  2.2.7
  2.2.8
  2.2.9
  2.2.10
  2.3.0-dev
  2.3.0-preview1
  2.3.0-preview2
  2.3.0
  2.3.1
  2.3.2
  2.3.3
  2.3.4
  2.3.5
  2.3.6
  2.3.7
  2.3.8
  2.4.0-dev
  2.4.0-preview1
  2.4.0-preview2
  2.4.0-preview3
  2.4.0-rc1
  2.4.0
  2.4.1
  2.4.2
  2.4.3
  2.4.4
  2.4.5
  2.4.6
  2.5.0-dev
  2.5.0-preview1
  2.5.0-rc1
  2.5.0
  2.5.1
  2.5.2
  2.5.3
  2.5.4
  2.5.5
  2.6.0-dev
  2.6.0-preview1
  2.6.0-preview2
  2.6.0-preview3
  2.6.0-rc1
  2.6.0-rc2
  2.6.0
  2.6.1
  2.6.2
  2.6.3
  2.7.0-dev
  jruby-1.5.6
  jruby-1.6.3
  jruby-1.6.4
  jruby-1.6.5
  jruby-1.6.5.1
  jruby-1.6.6
  jruby-1.6.7
  jruby-1.6.7.2
  jruby-1.6.8
  jruby-1.7.0-preview1
  jruby-1.7.0-preview2
  jruby-1.7.0-rc1
  jruby-1.7.0-rc2
  jruby-1.7.0
  jruby-1.7.1
  jruby-1.7.2
  jruby-1.7.3
  jruby-1.7.4
  jruby-1.7.5
  jruby-1.7.6
  jruby-1.7.7
  jruby-1.7.8
  jruby-1.7.9
  jruby-1.7.10
  jruby-1.7.11
  jruby-1.7.12
  jruby-1.7.13
  jruby-1.7.14
  jruby-1.7.15
  jruby-1.7.16
  jruby-1.7.16.1
  jruby-1.7.16.2
  jruby-1.7.17
  jruby-1.7.18
  jruby-1.7.19
  jruby-1.7.20
  jruby-1.7.20.1
  jruby-1.7.21
  jruby-1.7.22
  jruby-1.7.23
  jruby-1.7.24
  jruby-1.7.25
  jruby-1.7.26
  jruby-1.7.27
  jruby-9.0.0.0.pre1
  jruby-9.0.0.0.pre2
  jruby-9.0.0.0.rc1
  jruby-9.0.0.0.rc2
  jruby-9.0.0.0
  jruby-9.0.1.0
  jruby-9.0.3.0
  jruby-9.0.4.0
  jruby-9.0.5.0
  jruby-9.1.0.0-dev
  jruby-9.1.0.0
  jruby-9.1.1.0
  jruby-9.1.2.0
  jruby-9.1.3.0
  jruby-9.1.4.0
  jruby-9.1.5.0
  jruby-9.1.6.0
  jruby-9.1.7.0
  jruby-9.1.8.0
  jruby-9.1.9.0
  jruby-9.1.10.0
  jruby-9.1.11.0
  jruby-9.1.12.0
  jruby-9.1.13.0
  jruby-9.1.14.0
  jruby-9.1.15.0
  jruby-9.1.16.0
  jruby-9.1.17.0
  jruby-9.2.0.0-dev
  jruby-9.2.0.0
  jruby-9.2.1.0-dev
  jruby-9.2.1.0
  jruby-9.2.3.0
  jruby-9.2.4.0
  jruby-9.2.4.1
  jruby-9.2.5.0
  jruby-9.2.6.0
  jruby-9.2.7.0
  maglev-1.0.0
  maglev-1.1.0-dev
  maglev-2.0.0-dev
  mruby-dev
  mruby-1.0.0
  mruby-1.1.0
  mruby-1.2.0
  mruby-1.3.0
  mruby-1.4.0
  mruby-1.4.1
  mruby-2.0.0
  mruby-2.0.1
  rbx-2.2.2
  rbx-2.2.3
  rbx-2.2.4
  rbx-2.2.5
  rbx-2.2.6
  rbx-2.2.7
  rbx-2.2.8
  rbx-2.2.9
  rbx-2.2.10
  rbx-2.3.0
  rbx-2.4.0
  rbx-2.4.1
  rbx-2.5.0
  rbx-2.5.1
  rbx-2.5.2
  rbx-2.5.3
  rbx-2.5.4
  rbx-2.5.5
  rbx-2.5.6
  rbx-2.5.7
  rbx-2.5.8
  rbx-2.6
  rbx-2.7
  rbx-2.8
  rbx-2.9
  rbx-2.10
  rbx-2.11
  rbx-2.71828182
  rbx-3.0
  rbx-3.1
  rbx-3.2
  rbx-3.3
  rbx-3.4
  rbx-3.5
  rbx-3.6
  rbx-3.7
  rbx-3.8
  rbx-3.9
  rbx-3.10
  rbx-3.11
  rbx-3.12
  rbx-3.13
  rbx-3.14
  rbx-3.15
  rbx-3.16
  rbx-3.17
  rbx-3.18
  rbx-3.19
  rbx-3.20
  rbx-3.21
  rbx-3.22
  rbx-3.23
  rbx-3.24
  rbx-3.25
  rbx-3.26
  rbx-3.27
  rbx-3.28
  rbx-3.29
  rbx-3.30
  rbx-3.31
  rbx-3.32
  rbx-3.33
  rbx-3.34
  rbx-3.35
  rbx-3.36
  rbx-3.37
  rbx-3.38
  rbx-3.39
  rbx-3.40
  rbx-3.41
  rbx-3.42
  rbx-3.43
  rbx-3.44
  rbx-3.45
  rbx-3.46
  rbx-3.47
  rbx-3.48
  rbx-3.49
  rbx-3.50
  rbx-3.51
  rbx-3.52
  rbx-3.53
  rbx-3.54
  rbx-3.55
  rbx-3.56
  rbx-3.57
  rbx-3.58
  rbx-3.59
  rbx-3.60
  rbx-3.61
  rbx-3.62
  rbx-3.63
  rbx-3.64
  rbx-3.65
  rbx-3.66
  rbx-3.67
  rbx-3.68
  rbx-3.69
  rbx-3.70
  rbx-3.71
  rbx-3.72
  rbx-3.73
  rbx-3.74
  rbx-3.75
  rbx-3.76
  rbx-3.77
  rbx-3.78
  rbx-3.79
  rbx-3.80
  rbx-3.81
  rbx-3.82
  rbx-3.83
  rbx-3.84
  rbx-3.85
  rbx-3.86
  rbx-3.87
  rbx-3.88
  rbx-3.89
  rbx-3.90
  rbx-3.91
  rbx-3.92
  rbx-3.93
  rbx-3.94
  rbx-3.95
  rbx-3.96
  rbx-3.97
  rbx-3.98
  rbx-3.99
  rbx-3.100
  rbx-3.101
  rbx-3.102
  rbx-3.103
  rbx-3.104
  rbx-3.105
  rbx-3.106
  rbx-3.107
  ree-1.8.7-2011.03
  ree-1.8.7-2011.12
  ree-1.8.7-2012.01
  ree-1.8.7-2012.02
  topaz-dev
  truffleruby-1.0.0-rc10
  truffleruby-1.0.0-rc11
  truffleruby-1.0.0-rc12
  truffleruby-1.0.0-rc13
  truffleruby-1.0.0-rc14
  truffleruby-1.0.0-rc15
  truffleruby-1.0.0-rc16
  truffleruby-1.0.0-rc2
  truffleruby-1.0.0-rc3
  truffleruby-1.0.0-rc5
  truffleruby-1.0.0-rc6
  truffleruby-1.0.0-rc7
  truffleruby-1.0.0-rc8
  truffleruby-1.0.0-rc9

対策

ruby-buildがrubyのビルドに必要(?後で調べます・・・・・・・)とのことなので、下記コマンドを打って解決した

brew upgrade ruby-build

参考文献

https://github.com/rbenv/ruby-build

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

serverspecを導入する

Linux環境にserverspecを導入していく。

Linux環境の構築

Linux環境を構築しておく。
https://qiita.com/mkuser9/items/079cc4244821c8e220c2

rubyをインストールする

serverspecにはrubyが必要。

console
$ sudo yum -y install git
$ sudo yum install -y gcc gcc-c++ libyaml-devel libffi-devel libxml2 libxslt libxml2-devel libslt-devel
$ sudo yum install git-core
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l
$ source ~/.bash_profile
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ sudo yum install -y bzip2 gdbm-devel openssl-devel libffi-devel libyaml-devel ncurses-devel readline-devel zlib-devel
$ RUBY_CONFIGURE_OPTS=--disable-install-doc ~/.rbenv/bin/rbenv install 2.6.3
$ rbenv global 2.6.3 && rbenv rehash

Gemfileの作成

Gemfileを作成して、gemを追加する。

console
$ sudo vi Gemfile
Gemfile
source "https://rubygems.org"

gem 'serverspec'
gem 'rake'
console
$ bundle install

serverspecのインストール

以下のコマンドでserverspecをインストール。

console
$ gem install serverspec

サンプルテストの作成

console
$ serverspec-init

Select OS type:

  1) UN*X
  2) Windows

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 2

Vagrant instance y/n: n
Input target host name: www.example.jp
 + spec/
 + spec/www.example.jp/
 + spec/www.example.jp/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

テスト内容を記述

テストファイルは、spec/localhost/sample_spec.rbに生成されているので、テストの内容に合わせてテストファイルを修正する。

console
vi spec/localhost/sample_spec.rb

sample_spec.rbにテストを記述する。

sample_spec.rb(sample)
require 'spec_helper'

describe "gitについてのテスト" do
  context "環境設定" do
    describe package('git'), :if => os[:family] == 'redhat' do
      it "インストールされている" do
        expect be_installed
      end
    end
  end
end

describe "git-coreについてのテスト" do
  context "環境設定" do
    describe package('git-core'), :if => os[:family] == 'redhat' do
      it "インストールされている" do
        expect be_installed
      end
    end
  end
end

describe "httpdについてのテスト" do
  context "環境設定" do
    describe package('httpd'), :if => os[:family] == 'redhat' do
      it "インストールされている" do
        expect be_installed
      end

      it "enabledされている" do
        expect be_enabled
      end

      it "runningされている" do
        expect be_running
      end
    end
  end
end

describe "portについてのテスト" do
  context "環境設定" do
    describe port(80) do
      it "listeningされている" do
        expect be_listening 
      end
    end
  end
end

sample_spec.rbの初期状態。

sample_spec.rb(default)
require 'spec_helper'

describe package('httpd'), :if => os[:family] == 'redhat' do
  it { should be_installed }
end

describe package('apache2'), :if => os[:family] == 'ubuntu' do
  it { should be_installed }
end

describe service('httpd'), :if => os[:family] == 'redhat' do
  it { should be_enabled }
  it { should be_running }
end

describe service('apache2'), :if => os[:family] == 'ubuntu' do
  it { should be_enabled }
  it { should be_running }
end

describe service('org.apache.httpd'), :if => os[:family] == 'darwin' do
  it { should be_enabled }
  it { should be_running }
end

describe port(80) do
  it { should be_listening }
end

テストの実行

以下のコマンドでテストを実行する。

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

memo

serverspecを導入する。

Linux環境の構築

rubyをインストールする

serverspecにはrubyが必要。

console
$ sudo yum -y install git
$ sudo yum install -y gcc gcc-c++ libyaml-devel libffi-devel libxml2 libxslt libxml2-devel libslt-devel
$ sudo yum install git-core
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l
$ source ~/.bash_profile
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ sudo yum install -y bzip2 gdbm-devel openssl-devel libffi-devel libyaml-devel ncurses-devel readline-devel zlib-devel
$ RUBY_CONFIGURE_OPTS=--disable-install-doc ~/.rbenv/bin/rbenv install 2.6.3
$ rbenv global 2.6.3 && rbenv rehash

Gemfileの作成

console
$ sudo vi Gemfile
Gemfile
source "https://rubygems.org"

gem 'serverspec'
gem 'rake'
console
$ bundle install

serverspecのインストール

console
$ gem install serverspec

サンプルテストの作成

console
$ serverspec-init

Select OS type:

  1) UN*X
  2) Windows

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 2

Vagrant instance y/n: n
Input target host name: www.example.jp
 + spec/
 + spec/www.example.jp/
 + spec/www.example.jp/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

テスト内容を記述

console
vi spec/localhost/sample_spec.rb

sample_spec.rbにはすでにサンプルテストが書き込まれているため、適当に書き直す。

sample_spec.rb
require 'spec_helper'

describe "gitについてのテスト" do
  context "環境設定" do
    describe package('git'), :if => os[:family] == 'redhat' do
      it "インストールされている" do
        expect be_installed
      end
    end
  end
end

describe "git-coreについてのテスト" do
  context "環境設定" do
    describe package('git-core'), :if => os[:family] == 'redhat' do
      it "インストールされている" do
        expect be_installed
      end
    end
  end
end

describe "httpdについてのテスト" do
  context "環境設定" do
    describe package('httpd'), :if => os[:family] == 'redhat' do
      it "インストールされている" do
        expect be_installed
      end

      it "enabledされている" do
        expect be_enabled
      end

      it "runningされている" do
        expect be_running
      end
    end
  end
end

describe "portについてのテスト" do
  context "環境設定" do
    describe port(80) do
      it "listeningされている" do
        expect be_listening 
      end
    end
  end
end

sample_spec.rbの初期状態。

sample_spec.rb(default)
require 'spec_helper'

describe package('httpd'), :if => os[:family] == 'redhat' do
  it { should be_installed }
end

describe package('apache2'), :if => os[:family] == 'ubuntu' do
  it { should be_installed }
end

describe service('httpd'), :if => os[:family] == 'redhat' do
  it { should be_enabled }
  it { should be_running }
end

describe service('apache2'), :if => os[:family] == 'ubuntu' do
  it { should be_enabled }
  it { should be_running }
end

describe service('org.apache.httpd'), :if => os[:family] == 'darwin' do
  it { should be_enabled }
  it { should be_running }
end

describe port(80) do
  it { should be_listening }
end

テストの実行

以下のコマンドでテストを実行する。

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

#Ruby nested class and module pattern : child class or module not overwrite parent class or module : but able to define it

```rb
# A is class

class A
end

A.class
# => Class



# A is class and B is class

class A::B
end

A.class
# => Class

A::B.class
# => Class


# A is class but C is module

module A::C
end

A.class
# => Class

A::B.class
# => Class

A::C.class
# => Module

A::C.ancestors
# => [A::C]


# Unable to replace class to module or module to class


module A
end
# TypeError: A is not a module

module A::B
end
# TypeError: B is not a module

class A::C
end
# TypeError: C is not a class



# when Module is parent pattern

module D
end

module D::E
end

class D::F
end

D.class
# => Module

D::E.class
# => Module

D::F.class
# => Class

# Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2827
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Ruby で class と module の定義をネストさせることは可能だが上書きはできない

# A is class

class A
end

A.class
# => Class



# A is class and B is class

class A::B
end

A.class
# => Class

A::B.class
# => Class


# A is class but C is module

module A::C
end

A.class
# => Class

A::B.class
# => Class

A::C.class
# => Module

A::C.ancestors
# => [A::C]


# Unable to replace class to module or module to class


module A
end
# TypeError: A is not a module

module A::B
end
# TypeError: B is not a module

class A::C
end
# TypeError: C is not a class



# when Module is parent pattern

module D
end

module D::E
end

class D::F
end

D.class
# => Module

D::E.class
# => Module

D::F.class
# => Class

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2826

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

サンプルコードでわかる!Ruby 2.7の新機能・パターンマッチ(前編)

はじめに

Rubyは毎年12月25日にアップデートされます。
Ruby 2.7については2019年11月23日にpreview3がリリースされました。

Ruby 2.7.0-preview3 リリース

この記事ではRuby 2.7で導入される変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。

ただし、Ruby 2.7は多くの新機能や変更点があり、1つの記事に収まらないのでいくつかの記事に分けて書いていきます。
本記事で紹介するのはパターンマッチ(もしくはパターンマッチング)です。

前編と後編にわかれています

パターンマッチは説明する内容が多いので、次のように前編と後編の2部構成になっています。

  • 前編 = パターンマッチの概要、case文っぽい使い方、配列やハッシュとのマッチ、変数への代入
  • 後編 = 自作クラスをパターンマッチで活用する方法、パターン名の整理

パターンマッチ以外のRuby 2.7の新機能はこちら

Ruby 2.7ではパターンマッチ以外にもさまざまな新機能や変更点があります。
それらについては以下の記事で紹介しています。こちらもあわせてご覧ください。

本記事の情報源

本記事は以下のような情報源をベースにして、記事を執筆しています。
(下記の情報源はいずれもRubyにおけるパターンマッチの提唱者・開発者である@k_tsjさんによるものです)

動作確認したRubyのバージョン

本記事は以下の環境で実行した結果を記載しています。

$ ruby -v
ruby 2.7.0preview3 (2019-11-23 master b563439274) [x86_64-darwin19]

フィードバックお待ちしています

本文の説明内容に間違いや不十分な点があった場合は、コメント欄や編集リクエスト等で指摘 or 修正をお願いします?

それでは以下が本編です!

Step 0. パターンマッチの概要

Ruby 2.7では新しくパターンマッチ構文が導入されました。
構文自体はこのあとで詳しく説明するので、まずここではパターンマッチの概要について説明しておきます。

パターンマッチとは

パターンマッチ(もしくはパターンマッチング)は関数型言語でよく使われる条件分岐の一種です。
Wikipediaには「多分岐の一種で、場合分けと同時に構成要素の取り出しのできる言語機能」という説明があります。

また、Rubyにおけるパターンマッチはざっくりいうと、「多重代入ができるcase文」です(参考)。

ただ、この説明だけではほとんどの人はまだピンと来ないと思います。
パターンマッチの構文や機能的な説明については、このあとで詳しく説明していきます。

ざっくりとした構文説明

Rubyのパターンマッチは次のようにcase/inを使います。

case [0, [1, 2]]
in [0, [1, a]]
  # パターンがマッチした場合の処理
else
  # それ以外の処理
end

見た目上はcase文によく似ていますが(そしてcase文とよく似た使い方もできますが)、機能的には別物です。
上のサンプルコードでもこれまでのcase文では説明が付かない点が隠れています。
具体的にパターンマッチでどんなことができるのかは、このあとの記事の中で説明していきます。

後方互換性に十分配慮した設計になっている

Rubyのパターンマッチは今回新しく導入された機能であるため、新しい予約語を追加したりすると、既存のRubyのコードが動かなくなる可能性があります。
そのため、caseinといった既存の予約語を活用するなどして、後方互換性に十分配慮した設計になっています。

また、Rubyが元から有している配列やハッシュのパワフルさを活用したり、静的な型よりもダックタイピングを重視したりするなど、「Rubyらしさを失わないこと」も設計の重要なポイントのひとつになっているそうです。

こうした設計上の詳しい裏話は下記スライドの33枚目〜44枚目を参照してください。

https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=33

Ruby 2.7ではまだ試験段階

Ruby 2.7の時点では、パターンマッチはまだ試験的(experimental)な実装となっています。
将来的に仕様が変更される可能性があるため、パターンマッチ構文を使うとその都度警告が発生します。

# パターンマッチ構文を使うとその都度警告が出る
case 0
in 0
  'zero'
end
#=> warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

また、パフォーマンスも改善の余地が大きいとのことです。(参考
ですので、実務で書くコードにパターンマッチ構文を組み込むのは、Ruby 2.7の時点ではまだ避けた方が良いかもしれません。

パターンマッチの提唱者・開発者の@k_tsjさんいわく「実際に使って気になった点をフィードバックしてほしい」とのことですので(参考)、「こういうユースケースでこんなふうに使いたい」「ここの挙動がちょっと気になる」等の意見があれば、積極的にフィードバックを上げていくのが良いと思います。

パターンマッチの概要については以上です。
それではこれからパターンマッチの使い方を説明していきます。

Step 1. 一種のcase文として使う

パターンマッチはcase文っぽく使うことができます。

# case文
case 1
when 0
  'zero'
when 1
  'one'
end
#=> one

# パターンマッチ
case 1
in 0
  'zero'
in 1
  'one'
end
#=> one

whenがinに変わったところ以外は同じですね。
else節で「それ以外の条件」を指定したり、範囲オブジェクトやクラス名(定数)が渡せるところも同じです。

# 範囲オブジェクトで比較する例 ===========
# case文
case 2000
when 1926..1988
  '昭和'
when 1989..2012
  '平成'
else
  '令和'
end
#=> 平成

# パターンマッチ
case 2000
in 1926..1988
  '昭和'
in 1989..2012
  '平成'
else
  '令和'
end
#=> 平成

# クラス名(定数)で比較する例 ===========
# case文
case 'abc'
when Time
  '時間です'
when Integer
  '整数です'
when String
  '文字列です'
end
#=> 文字列です

# パターンマッチ
case 'abc'
in Time
  '時間です'
in Integer
  '整数です'
in String
  '文字列です'
end
#=> 文字列です

少し技術的なことを説明すると、上の例ではcase文もパターンマッチも===で比較して真になれば節が実行されます。

(1989..2012) === 2000
#=> true

String === 'abc'
#=> true

また、case文もパターンマッチも、上から順に評価して最初に一致した節を実行する、という挙動は同じです。

# case文
case 'abc'
when Object
  'オブジェクトです'
when Integer
  '整数です'
when String
  # 文字列ではあるが、when Objectが先に一致する
  '文字列です'
end
#=> オブジェクトです

# パターンマッチ
case 'abc'
in Object
  'オブジェクトです'
in Integer
  '整数です'
in String
  # 文字列ではあるが、in Objectが先に一致する
  '文字列です'
end
#=> オブジェクトです

case文もパターンマッチも、thenを使って次のように書くことができます。

# case文
case 1
when 0 then 'zero'
when 1 then 'one'
end
#=> one

# パターンマッチ
case 1
in 0 then 'zero'
in 1 then 'one'
end
#=> one

複数の条件を並べることもできます。
ただし、パターンマッチでは,ではなく|で条件を並べます。

# case文
case 'melon'
when 'tomato', 'potato', 'carrot'
  '野菜です'
when 'orange', 'melon', 'banana'
  '果物です'
end
#=> 果物です

# パターンマッチ(,ではなく、|で条件を並べる)
case 'melon'
in 'tomato' | 'potato' | 'carrot'
  '野菜です'
in 'orange' | 'melon' | 'banana'
  '果物です'
end
#=> 果物です

パターンマッチではどの条件にも一致しなければ例外が発生する

さて、ここまでは「case文もパターンマッチもほとんど同じです」というような説明をしてきました。
しかし、それではパターンマッチを使う理由がほとんどありません。
上で説明してきたようなコード例であれば、従来通りのcase文を使う方がよいと思います。

大きな違いがあるとすれば、どの条件にも一致しなかったときの振る舞いです。
以下のコード例を見てください。

# case文(どの条件にも一致しなければ何も起きずnilが返る)
case 'chicken'
when 'tomato', 'potato', 'carrot'
  '野菜です'
when 'orange', 'melon', 'banana'
  '果物です'
end
#=> nil

# パターンマッチ(どの条件にも一致しなければ例外が発生する)
case 'chicken'
in 'tomato' | 'potato' | 'carrot'
  '野菜です'
in 'orange' | 'melon' | 'banana'
  '果物です'
end
#=> NoMatchingPatternError (chicken)

上のように、パターンマッチではどの条件にも一致せず、なおかつelse節も用意されていなかった場合は、NoMatchingPatternErrorが発生します。
ですので、「予想外の値が渡ってきたら例外を発生させる」というコードをわざわざ書かずに済みます。

たとえば以下はタスクのステータスに応じて条件分岐させる架空のコード例です。

# case文で書く場合
case task.status
when :todo
  # ...
when :doing
  # ...
when :done
  # ...
else
  # 知らない間に新しいstatusが追加された可能性があるので
  # 例外を発生させる(考慮漏れの防止)
  raise "Unknown status: #{task.status}"
end

# パターンマッチで書く場合
case task.status
in :todo
  # ...
in :doing
  # ...
in :done
  # ...
# else
  # 考慮漏れがあれば例外が発生するのでelseは不要
end

Step 1の説明は以上です。
ここではパターンマッチを一種のcase文として使う方法を説明しました。
しかし、本当にパターンマッチらしい使い方は次のStep 2からになります。

Step 2. 配列やハッシュの構造で分岐させる + 変数に代入する

Step 1では、あえてcase文とよく似た使い方を説明しました。
構文もよく似ているので、一見するとcase文とパターンマッチは同じような機能しかないように見えるかもしれません。
ですが、このStep 2ではcase文とは全く異なるパターンマッチの使い方を説明していきます。

まずは配列でパターンマッチを使ってみましょう。

# 配列とパターンマッチを組み合わせる
case [3, 4, 5]
in [0, 1, 2]
  '0-2'
in [3, 4, 5]
  # ここが実行される
  '3-5'
in [6, 7, 8]
  '6-8'
end
#=> 3-5

上の例ではin [3, 4, 5]の節が実行されました。
でも、これだとcase文と構文以外の差異はないですし(inをwhenに変えても同じ結果になります)、特に違和感はありませんね。

それでは次のコードはどうでしょうか?

case [3, 4, 5]
in [0, 1, n]
  '0-2'
in [3, 4, n]
  # ここが実行される(のはなぜ??)
  '3-5'
in [6, 7, n]
  '6-8'
end
#=> 3-5

突然、変数らしきnが登場しました。
ですが、n = 1のようなローカル変数の宣言はどこにもありません。(書き忘れているわけではないですよ!)
そして、どういうわけかin [3, 4, n]の節が実行されています。

つづいて、上のコードを次のように変えてみましょう。

case [3, 4, 5]
in [0, 1, n]
  "0, 1 and #{n}"
in [3, 4, n]
  # nに5が代入される(のはなぜ??)
  "3, 4 and #{n}"
in [6, 7, n]
  "6, 7 and #{n}"
end
#=> 3, 4 and 5

なんと、"3, 4 and #{n}"n5が代入されています。
それなら、case [3, 4, 5]case [3, 4, 100]に変えてみるとどうでしょうか?

case [3, 4, 100]
in [0, 1, n]
  "0, 1 and #{n}"
in [3, 4, n]
  # nに100が代入される(のはなぜ??)
  "3, 4 and #{n}"
in [6, 7, n]
  "6, 7 and #{n}"
end
#=> 3, 4 and 100

今度はn100が代入されました!

では、そろそろちゃんと上のコードの意味を説明しましょう。
上のパターンマッチで使われているin [3, 4, n]の意味は次のように解釈してください。

  • 要素が3つの配列で、最初の2つが34であれば、マッチしたものと見なす
  • 3つ目の要素は任意。5でも100でも10000でもよい
  • さらに、3つ目の要素をローカル変数nに代入する

これは今までのRuby文法にはなかった、まったく新しい仕組みです。
表面的にはcase文っぽく見えるので、つい頭の中で「従来のcase文の考え方」に当てはめたくなるかもしれませんが、いったんその考えは捨ててください。

「要素が3つの配列で、最初の2つが3と4、3つ目は任意」という、「オブジェクト(ここでは配列)の構造」を条件分岐の判定条件に使っている点と、=演算子なしでローカル変数への代入が実現できている点が、パターンマッチのまったく新しい考え方になります。
(ピンと来ない人はピンと来るまで、上のコードと説明を繰り返し読み直しましょう!)

コラム:代入ではなく束縛?

ちなみに、パターンマッチはもともと関数型言語でよく使われている構文です。
関数型言語では「変数への代入」ではなく「変数束縛(variable binding)」という言い方をします。

in [3, 4, n]は、「nに5を代入する」というよりも、「構造的に一致した3つ目の値をつかまえて、変数nに縛りつけた(束縛した)」というふうに解釈した方が、個人的にはしっくりくるような気がします。

ハッシュでも同じように構造で判定して、変数に代入できる

上の例では配列を使いましたが、ハッシュでも同じようなことを実現できます。

case {status: :error, message: 'User not found.'}
in {status: :success, message: message}
  "Success!"
in {status: :error, message: message}
  # ここが実行される
  "Error: #{message}"
end
#=> Error: User not found.

配列の例が理解できていれば、ハッシュの場合も解釈はそこまで難しくないと思います。
上のコードではin {status: :error, message: message}の節が実行されています。
これは次のように読み解いてください。

  • キーが:statusで値が:errorである要素と、キーが:messageで値は任意である要素が含まれるハッシュであれば、マッチしたものと見なす
  • キーが:messageである要素の値は、ローカル変数messageに代入される

ちなみに、変数として参照する必要がない任意の値は、_を使って無視することもできます。

# :statusが:successの場合
case {status: :success, message: ''}
in {status: :success, message: _}
  # :statusが:successの場合は:messageの値は任意かつ、参照しないので _ を使う
  "Success!"
in {status: :error, message: message}
  "Error: #{message}"
end
#=> Success!

さらに言うと、パターンマッチでハッシュを使う場合は「部分一致」でマッチしたかどうかが判定されます。
ですので、次のように書けばさらにシンプルになります。

case {status: :success, message: ''}
in {status: :success}
  # :statusがキーで値が:successの要素が含まれる(それ以外の要素は何でもよい)、
  # という条件に一致すれば、この節が実行される
  "Success!"
in {status: :error, message: message}
  "Error: #{message}"
end
#=> Success!

# ちなみに配列の判定条件は完全一致
case [0, 1, 2]
in [0, n]
  'NO'
in [0, 1, n]
  'YES'
end
#=> YES

ハッシュと配列を組み合わせる

パターンマッチの判定条件には配列とハッシュを組み合わせることもできます。

# 下記ページのサンプルコードを一部改変
# https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=10

data = {
  name: 'Alice',
  age: 30,
  zodiac: 'Capricorn',
  children: [
    {
      name: 'Bob',
      age: 10,
      zodiac: 'Aquarius'
    }
  ]
}

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  "Bob is #{age} years old."
end
#=> Bob is 10 years old.

上のサンプルコードのパターンマッチは、次のように解釈します。

  • キーが:nameで値が'Alice'である要素と、キーが:childrenで値が配列である要素が含まれるハッシュであり、さらにその配列の要素数は1つで、その要素がキーが:nameで値が"Bob"、そしてキーが:ageで値は任意の要素を含むハッシュになっていればマッチする
  • :childrenの配列の中に含まれるハッシュのキー:ageに対応する値は、ローカル変数ageに代入される

文章にすると結構複雑になりますね。
文章で理解しようとするよりも、コードをじっくり読んだ方がこの場合は近道になるかもしれません。

次に、ここまで説明したパターンマッチの使い方で、少し注意すべき点をいくつか説明します。

注意点1:ハッシュで使えるキーはシンボルのみ

Ruby 2.7の時点ではハッシュのキーにはシンボルしか使えないという制限があるので注意してください。
(これは後述する変数代入のための=>と、ハッシュリテラルの=>が構文的に衝突してしまうためです)

# シンボル以外のキーで比較しようとすると構文エラーが発生する
case {'status' => :success, 'message' => ''}
in {'status' => :success}
  "Success!"
end
#=> syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END (SyntaxError)
#       in {'status' => :success}
#                  ^

注意点2:パターンマッチで代入できる変数はローカル変数のみ

パターンマッチで代入できるのはローカル変数のみです。
次のようにインスタンス変数に代入しようとすると構文エラーが発生します。

# インスタンス変数に代入しようとすると構文エラーになる
case 0
in @a
  # ...
end
#=> syntax error, unexpected instance variable
#   in @a

ちなみに「ローカル変数以外の変数にもパターンマッチで代入できた方が便利なのでは?」という提案をしてみましたが、「パターンマッチがさらに複雑になってしまう」という理由でいったん却下されています。(参考

注意点3:マッチに失敗しても変数の代入は完了している

次のコードのようにマッチに成功しなかった分岐があっても、ローカル変数への代入は完了します。

case [0, 1, 2]
# この条件にはマッチしない(が、変数nへの代入は完了している)
in [n, 1, 3]
  'matched in'
else
  'matched else'
end
#=> matched else

# パターンマッチで使ったnに0が代入されている
n #=> 0

ただし、パターンマッチで使われた変数のスコープに関しては将来的に仕様が改善される可能性があります。

参考 https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=45

さて、パターンマッチに配列やハッシュを使ったり、変数に代入したりする場合の基本的な考え方は、だいたい説明しました。
ここからあとではさらに、応用的な使い方や発展的な使い方を説明していきます。

応用1:既出の変数の値そのものを条件に使う ^

先ほど、パターンマッチでは判定のタイミングでローカル変数への代入ができると説明しました。
ですので、みなさんはもう次のコードでやっていることが理解できるはずです。

case 10
in 0
  'zero'
in x
  # 0以外の任意のオブジェクトが変数xに代入される
  "#{x}です"
end
#=> 10です

では上のコードですでに変数xが宣言されているとどうなるでしょうか?
やってみましょう。

# 変数xを宣言しておく
x = 1

case 10
in 0
  'zero'
in x
  # xに新しい値(ここでは10)が代入される
  "#{x}です"
end
#=> 10です

# ローカル変数xの中身も書き換えられている
x
#=> 10

ご覧のとおり、既出の変数xはパターンマッチのタイミングで新しい値が代入されました。

しかし、場合によっては変数xそのものの値(上の例なら1)をパターンマッチの条件に使いたい、という場面もあると思います。
そういう場合は^xのように、変数の手前に^を付けます。

x = 1

case 10
in 0
  'zero'
in ^x
  # in 1 と書いたことに等しい(ので、マッチするパターンがなく例外が発生する)
  "#{x}です"
end
#=> NoMatchingPatternError (10)

case 1
in 0
  'zero'
in ^x
  # case 1 なので、今回はマッチする
  "#{x}です"
end
#=> 1です

ご覧のとおり、^を付けることで変数xの値そのものをパターンマッチの条件として使うことができました。

コラム:複雑な式(メソッド呼び出しなど)は直接in節に指定できない

パターンマッチでは次のようにメソッド呼び出しを含む式をin節に記述することはできません(構文エラーになります)。

case 1
# randメソッドの戻り値を直接in節に指定することはできない(構文エラー)
in rand(1..3)
  # ...
end
#=> syntax error, unexpected '(', expecting `then' or ';' or '\n'
#   in rand(1..3)
#   syntax error, unexpected ')', expecting end-of-input
#   in rand(1..3)

# ちなみにcase文であればwhen節に複雑な式も書ける(構文として有効)
case 1
when rand(1..3)
  # ...
end

ただし、次のようにいったん変数に入れて^演算子を使えば、もともと意図していたコードと同等のコードが書けます。

# randメソッドの戻り値をいったん変数nに入れる
n = rand(1..3)

# 変数nと^演算子を組み合わせれば、結果としてrandメソッドの戻り値で比較したことになる
case 1
in ^n
  # ...
end

この点については、将来的に^(rand(1..3))のような構文をサポートするかもしれないとのことです(参考)。

# 将来的にはこんな構文がサポートされるかも?
case 1
in ^(rand(1..3))
  # ...
end

応用2:マッチした構造の一部(または全体)を変数に代入する =>

パターンマッチでは=> (変数名)という構文を使うことで、マッチしたオブジェクトを変数に代入することができます。
たとえばこんな感じです。

case 123
in String
  # 文字列ではないのでマッチしない
in Integer => i
  # 123は整数(Integerクラスのインスタンス)なのでマッチする
  # さらに、マッチした値が変数iに代入される
  "#{i}です"
end
#=> 123です

配列やハッシュと=>を組み合わせると、マッチした構造の一部をまるっと変数に代入することもできます。
たとえば、先ほど「ハッシュと配列を組み合わせる」の項では次のようなコードを紹介しました。

data = {
  name: 'Alice',
  age: 30,
  zodiac: 'Capricorn',
  children: [
    {
      name: 'Bob',
      age: 10,
      zodiac: 'Aquarius'
    }
  ]
}

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  "Bob is #{age} years old."
end
#=> Bob is 10 years old.

このとき、:childrenの値である配列部分がほしいと思ったときはどうすればいいでしょうか?
もし、そのままやろうとすると次のようなコードになります。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  children = [{name: 'Bob', age: age}]
  # childrenを使った処理が続く...
end

しかし、上のコードはDRYではないのでイマイチですね。
こんなときは=>を使って変数に代入すると便利です。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}] => children}
  # [{name: 'Bob', age: age}] にあたる部分が変数childrenに代入される
  p children
  #=> [{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]
end

ただし、上の実行結果を見てもらえればわかるとおり、変数childrenの中身は[{:name=>"Bob", :age=>10}]だけではなく、inの条件には指定していない:zodiac=>"Aquarius"も含まれている点に注意してください。(つまり、data[:children]で得られる結果と同じになっています)

また、一番外側に=>を書けば、条件にマッチした構造全体を取得することができます。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]} => alice
  p alice
  #=> {:name=>"Alice", :age=>30, :zodiac=>"Capricorn", :children=>[{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]}
end

2つ以上の=>を同時に使うこともできます。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}] => children} => alice
  p alice
  #=> {:name=>"Alice", :age=>30, :zodiac=>"Capricorn", :children=>[{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]}

  p children
  #=> [{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]
end

配列でも同じように=>が使えます。

# 下記ページのサンプルコードを一部改変
# https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=23

case [0, [1, 2]]
in [0, [1, _] => arr]
  # マッチした内側の配列を変数arrに代入
  p arr
  #=> [1, 2]
end

case [0, [1, 2]]
in [0, [1, _]] => arr
  # マッチした配列全体を変数arrに代入
  p arr
  #=> [0, [1, 2]]
end

ただし、elseに=>を使うことはできません。(構文エラーになります)

case []
in String
  # ...
in Integer
  # ...
else => obj
  # else => は構文エラー
  "#{obj}です"
end
#=> syntax error, unexpected =>
#   else => obj

ちなみに上の例であれば、in objのように書けば任意の値を変数objに代入できます。

case []
in String
  # ...
in Integer
  # ...
in obj
  # 文字列でも整数でもないオブジェクトが変数objに代入される
  "#{obj}です"
end
#=> "[]です"

応用3:ガード条件を追加する

パターンマッチにはifやunlessを使ったガード条件を追加することができます。

# ageが20より大きいこと、というガード条件を付ける(マッチしない)
case {name: 'Alice', age: 20}
in {name: 'Alice', age: age} if age > 20
  'Hi, Alice!'
end
#=> NoMatchingPatternError ({:name=>"Alice", :age=>20})

# ageが20以下であること、というガード条件を付ける(マッチする)
case {name: 'Alice', age: 20}
in {name: 'Alice', age: age} if age <= 20
  'Hi, Alice!'
end
#=> Hi, Alice!

こちらはifの代わりにunlessを使う例です。

# ageが偶数でないこと、という条件を付ける(マッチしない)
case {name: 'Alice', age: 20}
in {name: 'Alice', age: age} unless age.even?
  'Hi, Alice!'
end
#=> NoMatchingPatternError ({:name=>"Alice", :age=>20})

=> で代入した変数もガード条件に使えます。

case 0
in Integer => n if n.zero?
  'zero'
end
#=> zero

応用4:*で配列のその他の要素を指定する

*を使うと配列のその他の要素を指定することができます。

case [0, 1, 2, 3, 4]
in [0, *rest]
  # 配列の要素が1個以上で最初の要素が0ならマッチする
  # さらに、2番目以降の要素は変数restに代入される
  p rest
end
#=> [1, 2, 3, 4]

case [0]
in [0, *rest]
  # 0以外の要素がないので、この場合はrestには空の配列が代入される
  p rest
end
#=> []

次のような少し凝った使い方も可能です。

case [0, 1, 2, 3, 4]
in [first, second, *rest, last]
  # 1番目、2番目、最後の要素をそれぞれ変数に入れる
  puts "1st: #{first}, 2nd: #{second}, last: #{last}"
  # それ以外の要素は配列として受け取る
  p rest
end
#=> 1st: 0, 2nd: 1, last: 4
#   [2, 3]

変数名なしで*だけで使うこともできます。

case [0, 1, 2, 3, 4]
in [*, 4]
  # 最後が4で終わる配列にマッチ(それより手前の要素は任意)
  'end with 4'
end
#=> end with 4

case [4]
in [*, 4]
  # この場合、配列の要素数は1個でも良い
  'end with 4'
end
#=> end with 4

応用5:**でハッシュの残りの要素を指定する / ハッシュを完全一致させる

配列の*と同じように、**を使うとハッシュの残りの要素を指定することができます。

case {japan: 'yen', us: 'dollar', india: 'rupee'}
in {japan: _, **rest}
  # キーに:japanを含むハッシュ(値は任意)にマッチ
  # さらに、キーが:japan以外の要素は変数restに代入される
  p rest
end
#=> {:us=>"dollar", :india=>"rupee"}

**を使う場合は最後の要素として記述します。
次のように途中で挟みこまれるような形で書くと構文エラーが発生します。

# **は最後に来ないと構文エラーが発生する
case {japan: 'yen', us: 'dollar', india: 'rupee'}
in {japan: _, **rest, us: _}
  p rest
end
#=> syntax error, unexpected ',', expecting '}'
#   in {japan: _, **rest, us: _}

# **が最後に来ているのでこれはOK
case {japan: 'yen', us: 'dollar', india: 'rupee'}
in {japan: _, us: _, **rest}
  p rest
end
#=> {:india=>"rupee"}

**nilと書くと、「それ以外の要素がないこと」を指定したことになります。
これにより、ハッシュのパターンマッチを部分一致ではなく、完全一致で指定することができます。

# これは部分一致
case {status: :success, message: ''}
in {status: :success}
  # 何も指定しない場合はハッシュは部分一致する
  # この場合は:statusがキーで値が:successの要素が含まれれば、それ以外の要素は何でもよい
  "Success!"
end
#=> Success

# これは完全一致
case {status: :success, message: ''}
in {status: :success, **nil}
  # **nilで「それ以外の要素がないこと」を指定する
  # この場合は:statusがキーで値が:successの、要素が1つだけあるハッシュでないとマッチしない
  "Success!"
end
#=> NoMatchingPatternError ({:status=>:success, :message=>""})

# これも完全一致
case {status: :success}
in {status: :success, **nil}
  # :statusがキーで値が:successの、要素が1つだけのハッシュなのでマッチ
  "Success!"
end
#=> Success

なお、in {}と書いた場合は「空のハッシュ」にマッチ(つまり完全一致)します。

case {}
in {}
  'empty'
end
#=> empty

応用6:一番外側の[]{}を省略する

配列やハッシュをinに書く場合は、一番外側の[]{}を省略できます。

case [0, [1, 2]]
# 一番外側の[]を省略しない場合
in [0, [a, b]]
  # ...
end

case [0, [1, 2]]
# 一番外側の[]を省略する場合
in 0, [a, b]
  # ...
end
case {name: 'Alice', age: 20}
# 一番外側の{}を省略しない場合
in {name: 'Alice', age: age}
  # ...
end

case {name: 'Alice', age: 20}
# 一番外側の{}を省略する場合
in name: 'Alice', age: age
  # ...
end

ただし、=>を使ってマッチしたオブジェクト全体を取得する場合は、外側の[]{}を書く必要があります。

case [0, [1, 2]]
in 0, [1, 2] => a
  # 外側の[]を省略したので、内側の[1, 2]がaに代入される
  p a
end
#=> [1, 2]

case [0, [1, 2]]
in [0, [1, 2]] => a
  # 外側の[]を書いたので、配列全体がaに代入される
  p a
end
#=> [0, [1, 2]]

case {a: {b: 2, c: 3}}
in a: {b: 2, c: _} => h
  # 外側の{}を省略したので、内側の{b: 2, c: 3}がhに代入される
  p h
end
#=> {:b=>2, :c=>3}

case {a: {b: 2, c: 3}}
in {a: {b: 2, c: _}} => h
  # 外側の{}を書いたので、ハッシュ全体がhに代入される
  p h
end
#=> {:a=>{:b=>2, :c=>3}}

応用7:変数名を省略し、暗黙的にハッシュのキーと同名の変数に代入する

パターンマッチでは変数名を省略し、暗黙的にハッシュのキーと同名の変数に代入することができます。
次の2つのコードはどちらも同じことをやっていますが、前者は明示的に変数名を指定し、後者は変数名を省略して暗黙的にキーと同名の変数に代入しています。

case {name: 'Alice', age: 20}
# 変数名を明示的に指定する
in name: name, age: age
  "#{name} is #{age} years old"
end
#=> Alice is 20 years old

case {name: 'Alice', age: 20}
# 変数名の指定を省略し、暗黙的にキーと同名の変数に代入することも可能
in name: , age:
  "#{name} is #{age} years old"
end
#=> Alice is 20 years old

また、パターンマッチは次のように1行で書くこともできます。

# 1行で書くときはcaseが不要
[0, 1, 2] in [0, 1, n]

この構文を利用すると、次のようにしてハッシュの値を変数に代入することができます。

data = {name: 'Alice', age: 20, zodiac: 'Capricorn'}

# 1行で書くパターンマッチを利用して、キーに対応する値を暗黙的に変数に代入する
# (ただし、この場合は{}を省略できない)
data in {name:, zodiac:}
"name=#{name}, zodiac=#{zodiac}"
#=> name=Alice, zodiac=Capricorn

# values_atメソッドでも同じことができるが、変数名を明示的に書く必要がある(ので、ちょっとDRYでない)
name, zodiac = data.values_at(:name, :zodiac)
"name=#{name}, zodiac=#{zodiac}"
#=> name=Alice, zodiac=Capricorn

【注意】
1行で書くパターンマッチの仕様はpreview3のリリース以後も変更が加えられており(参考)、最終的にどのような形になるのかはまだハッキリしないところがあるのでご注意ください。

続きは後編で!

前編は以上です。
今回はパターンマッチの概要、case文っぽい使い方、配列やハッシュとのマッチ、変数への代入といったトピックを説明しました。

パターンマッチの残りのトピック(自作クラスをパターンマッチで活用する方法と、パターン名の整理)は後編の記事で紹介します。

後編は明日12月10日に公開予定です!

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

Railsエンジニアの視点でHanamiを解説してみた

はじめに

新しい職場に来てもう少しで2ヶ月が経過します。現在の業務でHanamiを書き始め、何となく慣れてきたので、アドベンドカレンダーの機会にHanamiについて書いてみたいと思います。しかし、Hanamiはまだまだマイナーなフレームワークであるため、Railsエンジニアの視点で分かりやすいように解説していきます。

Hanamiとは

2017年4月にバージョン 1.0.0 がリリースされたばかりの比較的新しいRubyのフレームワークです。Railsとの違いで代表的なものはこんなところです。

長期的なメンテナンスに向いたフレームワーク

Rails はMVCやActiveRecordに仕様の大部分が依存したフレームワークになっています。 一方でHanami は DDD (ドメイン駆動設計) をベースにしつつ、ある程度柔軟性を残した状態で開発出来るフレームワークとなっています。 もう少し具体的に言うと、Rails はサービスを素早くローンチすることに向いている一方で Hanami は長期的な保守を念頭においた開発に向いているといった感じです。

ただし、Railsと同様にMVC全体がサポートされたフルスタックなフレームワークなので、必ずしも初期ローンチを犠牲にしている訳ではありません。最近では、マイクロサービスを念頭に置いた開発を行うようになってきたこともあり、初期ローンチとサービス成長の両方を見据えた選択肢としては良さそうなフレームワークです(ここはあくまでも2ヶ月間書いてきた人間の初感ですが。。)。

マジックが少ない

マジックと言うと少し分かりにくいですが、Rubyはメタプログラミングを上手く活用することで、プログラムを書くプログラムを書くことが可能です(初学者を混乱させてしまう説明かも。。)。もう少し具体的に言うと、実行時に与えられた引数に合わせて処理を柔軟に変化させるクラスやメソッドが書けるといった感じです(さらに分らなかったらごめんなさい。。)。メタプログラミングで書かれたクラスやメソッドは内部処理を知らなくても柔軟に使用出来てしまうことから、このような言われ方がされます。

特に、Railsは、ActiveRecordやActiveSupport系などの強力なGemによりマジックが生み出され、開発者が詳細を意識しなくても書けてしまうフレームワークになっています。このあたりがRailsは書けるけど、Rubyは書けないみたいなエンジニアが生まれてしまう原因だったり。。しかし、ビジネス的な視点で見ると、早くローンチ出来た方がビジネスでのトライ&エラーが出来るため、Railsが広まった大きな理由でもあり、問題というよりは利点だと思われます。

一方、Hanamiの場合は、一部で特定のGemに依存している部分は見られるものの、マジックが少なく、 ほぼPure Ruby に近いフレームワークとなっています。ちなみに、ActiveSupportが使えないため、一番最初に困るのが、present?blank?メソッドが使えなくなる点です。オブジェクトの種類を意識しないと無意識にNoMethodErrorを引き起こすコードを書きます。

あとは、長くなりそうなので、技術的な特徴などは公式ホームページをご覧下さい。

公式ホームページはこちら

hanamiコマンド

Hanamiでは、Railsと同様にhanamiコマンドが用意されています。細かいところで違いはあるものの、ほとんどrailshanamiになっただけです。

プロジェクトの作成
bundle exec hanami new . --database=mysql
Hanami console
bundle exec hanami console

ディレクトリ構造

上記のコマンドを実行した後のディレクトリ構造はこんな感じです。

今回、環境にdocker-composeを利用しています。dbディレクトリとDocker関係のファイルは気にしないで下さい。

apps配下にRailsでも馴染みがあるcontrollerstemplatesviewsが存在します。

そして、Railsと大きく違う点は、lib配下にDDDらしさが伺えるentitiesrepositoriesが存在する点です。簡単に説明すると、この2つがModelの役割を果たします。

.
|-- Dockerfile
|-- Gemfile
|-- Gemfile.lock
|-- README.md
|-- Rakefile
|-- apps
|   `-- web
|       |-- application.rb
|       |-- assets
|       |   |-- favicon.ico
|       |   |-- images
|       |   |-- javascripts
|       |   `-- stylesheets
|       |-- config
|       |   `-- routes.rb
|       |-- controllers
|       |-- templates
|       |   `-- application.html.erb
|       `-- views
|           `-- application_layout.rb
|-- config
|   |-- boot.rb
|   |-- environment.rb
|   `-- initializers
|-- config.ru
|-- db
|   |-- migrations
|   |-- mysql
|   |   `-- volumes
|   `-- schema.sql
|-- docker-compose.yml
|-- lib
|   |-- app
|   |   |-- entities
|   |   |-- mailers
|   |   |   `-- templates
|   |   `-- repositories
|   `-- app.rb
|-- public
`-- spec
    |-- app
    |   |-- entities
    |   |-- mailers
    |   `-- repositories
    |-- features_helper.rb
    |-- spec_helper.rb
    |-- support
    |   `-- capybara.rb
    `-- web
        |-- controllers
        |-- features
        `-- views
            `-- application_layout_spec.rb

Generater

これは、users#showというアクションを作成するコマンドです。もう、なんとなく分かると思いますが、RailsでいうところのControllerのActionです。

Hanamiの大きな特徴として、ControllerはAction毎にファイルを作ります。詳しくは、Controllerの章で解説します。

# bundle exec hanami generate action web users#show
      create  apps/web/controllers/users/show.rb
      create  apps/web/views/users/show.rb
      create  apps/web/templates/users/show.html.erb
      create  spec/web/controllers/users/show_spec.rb
      create  spec/web/views/users/show_spec.rb
      insert  apps/web/config/routes.rb

Routing

Railsと全く同じに書けます。詳細に調べて行くと、セッションの有無に応じてルーティングを行うような高度なルーティングも出来ますが、Railsと同じ感覚でこのあたりさえ覚えておけば、ほとんど使えてしまいそうです。

apps/web/config/route.rb
get '/',  to: "users#show"
post '/new', to: "users"

または

apps/web/config/route.rb
resources :users, only: [:show, :create]

Model

まずは、hanamiコマンドでModelを作成します。以下のコマンドを実行すると、entitiesrepositoriesが作成されます。

bundle exec hanami generate model user
      create  lib/src/entities/user.rb
      create  lib/src/repositories/user_repository.rb
      create  db/migrations/20191208133857_create_users.rb
      create  spec/src/entities/user_spec.rb
      create  spec/src/repositories/user_repository_spec.rb

Migration

そして、マイグレーションファイルもRailsとそれほど変わりません。

db/migrations/20191207110038_create_users.rb
Hanami::Model.migration do
  change do
    create_table :users do
      primary_key :id

      column :name,  String, null: false
      column :email, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

マイグレーションを実行するコマンドはこんな感じです。

bundle exec hanami db prepare

Entity

特殊な要件がなければ、これだけで使用出来ます。RailsのModelと同じように、Hanami::Entityを継承しているだけですが、面白い違いがあります。

class User < Hanami::Entity
end

Hanamiのconsoleで試して見ると、違いが分かります。Userクラスをインスタンス化する時に値は入れられるものの、後から代入しようとすると値が入りません。

つまり、Entityはイミュータブルな設計になっており、initializeの段階でしか値を入力することが出来ません。

つまり、Entityはデータをオブジェクトとして扱うためのただの入れ物です。

user = User.new(email: 'test@gmail.com')
user.email
#=> "test@gmail.com"
#=>
user.email = 'hoge@gmail.com'
#=> NoMethodError: undefined method `email='

Repository

それでは、どうやって値を代入するかですが、その役割はRepositoryが担います。つまり、DBへの更新はRepositoryの役割であり、SQLを発行するのもここが担います。

createupdatedeleteallfindなどの基本メソッドは、Hanami::Repositoryを継承するだけで予めサポートされます。他に必要なメソッドは以下のように自作する必要があります。

また、ここに複雑なロジックを噛ませることも可能です。しかし、ここにトリッキーなメソッドを定義するのは自殺行為なのでやめた方がいいです。ここでバグを仕込むとDBに保存されるデータが汚染され、恐ろしいことになるので、ロジックは出来るだけ別階層に書くべきです。遊びで試したところ、思い切ったクエリも実行出来るので下手なことをすると地獄を見そうで怖いです。

lib/src/repositories/user_repository.rb
class UserRepository < Hanami::Repository
  def find_by_email(email)
    users.where(email: email).first
  end
end

ちなみに使うときはこんな感じです。

user_repository = UserRepository.new
@user = user_repositor.find_by_email("test@gmail.com")

View

ViewはRailsとほとんど同じです。デフォルトのTemplateに、eRuby(.erb)が使用されています。
大きな違いはViewクラスが用意されている点です。

apps/web/views/users/show.rb
module Web
  module Views
    module Users
      class Show
        include Web::View

        def title
          'あるユーザーの情報'
        end
      end
    end
  end
end

あとはerbから呼び出すだけで使用できます。Rails経験者は勘が働くと思いますが、<h2>タグで呼び出されているコードはControllerで説明します。

apps/web/templates/users/show.html.erb
<h1><%= title %></h1>
<h2><%= user.name %></h2>
<h2><%= user.email %></h2>

どうやら、Viewクラスが存在することで、パーシャル化を行う際に非常に重宝するとの解説が見られますが、業務であまりいじらない箇所なので詳細な知見までは把握出来ていません。

Controller

Generaterの章で少し触れますが、HanamiのContorollerはAction毎に別ファイルに記載します。RailsだとControllerクラス配下のメソッドで定義されていたものが、Actionクラス毎に定義されるのだと理解すると分かりやすいと思います。

apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action

    params do
      required(:id).filled(:str?)
    end

    expose :user

    def call(params)
      if params.valid?
        redirect_to '/'
      else
        self.status = 422
        return
      end

      @user = UserRepository.new.find(params(:id))
    end
  end
end

フォームから送信された値がcallメソッドに引数として渡され、処理が行われます。このとき、params.valid?で以下のように定義されたValidationが実行されます。

今回は文字列かどうかのチェックだけが行われていますが、オリジナルのメソッドを定義すれば、有効なEmailアドレスの文字列が不正ではないかなどのチェックもここで可能です。

params do
  required(:id).filled(:str?)
end

そして、Viewの章で少し触れていますが、インスタンスメソッドを以下のように定義することで、View側に渡すことが出来ます。そして、Railsと同様にテンプレート内で使用が可能です。

ちなみに、Railsと違い、ここで定義しないとインスタンス変数は外に出ませんので注意が必要です。

expose :user

Interactor

最後に、一番馴染みがない名前が登場します。この用語の解説を始めると、ドメイン駆動設計の話やクリーンアーキテクチャの話に広がってしまうため、今回は省略します(私も勉強中のため、説明出来るほど理解が進んでいない。。)。

そこで、ここでは、RepositoryとControllerの間に入る層だと思えば、少し理解しやすいと思います。

また、Repositoryの章で複雑なロジックは、Repositoryクラスに書かないように注意を促しましたが、HanamiのビジネスロジックはこのInteractorに書くことになります。

例えば、Controllerの章で例にしたActionのロジックを移設するとこんな感じです。

lib/bookshelf/interactors/user/show.rb
require 'hanami/interactor'

module UserInteractor
  class Show
    include Hanami::Interactor

    expose :user

    def call(id)
      @user = UserRepository.new.find(id)
    end
  end
end

そして、Controllerはこのようになります。

apps/web/controllers/users/show.rb
module Web::Controllers::Users
  class Show
    include Web::Action
    include Hanami::Interactor

    params do
      required(:id).filled(:str?)
    end

    expose :user

    def call(params)
      if params.valid?
        redirect_to '/'
      else
        self.status = 422
        return
      end

      @user = UserInteractor::Show.new.call(params[:id])
    end
  end
end

業務上の経験でしかありませんが、基本的にビジネスロジックはInteractorに詰め込んでいくため、ちょっとした仕様変更だとここを弄るだけで済んでしまうことがあり、個人的にはHanamiの良さだと思っています。

最後に

長い解説になってしまいすみません。。もし、少しでも興味を持っていただけるのなら、Hanamiに触れてみてください。

また、今回は、Advent Calendar に間に合わせるために、サンプルを用意出来ませんでしたが、検証用のdocker-composeとサンプルリポジトリをあとで追加したいと思います。

以上です。

参考文献

HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
Hanami Getting Started guide

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

C - HonestOrUnkind2

感想

今日出たABC147は2問しか解けなかった
解けなさすぎて涙出てくるホント
bit全探索だってさ。なにそれ
あと配列の個数を死ぬほど間違えて死にたくなる

問題

スクリーンショット 2019-12-09 1.04.02.png

回答

N = gets.to_i
I = 2**N - 1
alist=[]
xlist=[]
ylist=[]
for i in 0..N-1
  A = gets.to_i
  X,Y = A.times.map{gets.split.map(&:to_i)}.transpose
  alist.push(A)
  xlist.push(X)
  ylist.push(Y)
end
result = 0
for i in 0..I
  breakflag = false
  S = []
  N.times do |j|
    if i & 1 == 1
      S.push(1)
    else
      S.push(0)
    end
    i = i >> 1
  end
  # 検査開始
  for n in 0..N-1
    if S[n] == 1
      for a in 0..alist[n]-1
        if S[xlist[n][a]-1] != ylist[n][a]
          breakflag = true
          break
        end
      end
    end
    break if breakflag
  end
  if breakflag == false
    if result < S.count(1)
      result = S.count(1)
    end
  end
end
p result

結果

スクリーンショット 2019-12-09 1.05.41.png

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