- 投稿日:2019-12-09T23:51:28+09:00
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アプリの紹介
今回私が製作したのは「このはちゃんに愛を叫びたい(通称:このさけ)」です!
Webアプリとしてはシンプルでこのはちゃんへの愛を投稿するシステムになってます。
シンプルな分読み込みのパフォーマンスも高く、デスクトップVerのPageSpeed Insightsでは常に95%以上を叩き出すくらい速いです。
このアプリと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 masteryes/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の設定」までを繰り返すことでいくつでもアプリをデプロイすることができます。
仕組みとしては全てのコンテナにポート番号が割り振られているためです。
ドメインもアクセスしたドメインを検知して自動的にアプリとつなげているのだと思います。
今回は詳しくは扱いませんが参考にしてみてください。
因みにこれを行うためにはスペックの良いサーバーを使うようにしてください。
管理するアプリが多すぎて処理しきれない場合があります。早速愛を叫びます。
早速、ある意味本題のこのはちゃんに「愛」を叫ぶってことをやっていきたいと思います。
私は控えめに叫びましたが、皆さんはぜひもっともっと熱く叫んでください!
投稿は以下で行うことができます!
このはちゃんに叫びたいさいごに
今回はConoHaVPSのDokkuテンプレートを用いてPaaSの構築を行ってみました。
初めての記事ちゃんと書けてたちょっと心配です.....。
けれど、少なからずConoHaのアプリケーションテンプレートの利便性と、自分でPaaSを作って自分のアプリをデプロイする楽しさってのは伝わったんじゃないかなって思ってます。
だからこの記事を読んでる皆さんも是非この楽しさを1回体感してみてください!来年も参加できる機会があれば参加したいと思います!
どうもありがとうございました!p.s.
何かあればお気軽にコメントよろしくお願いします。
是非Twitterなどにも足を運んでいただけると励みになります?
- 投稿日:2019-12-09T23:09:00+09:00
フィボナッチ数列を計算する
計算量について考えるネタとして、フィボナッチ数列を取り上げます。なお、多倍長整数など環境的にちょうどいいのと、作者が慣れているという理由で、コードは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
- 投稿日:2019-12-09T22:26:16+09:00
rails-tutorial第14章
ユーザーをフォローする
まずはRelationshipモデルを作っていこう
$ rails generate model Relationship follower_id:integer followed_id:integer
作成されたマイグレーションファイルにインデックスを書き足していく。
db/migrate/[timestamp]_create_relationships.rbclass 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.rbclass Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" endbelongs_to :followerとすると、Relationshipのfollower_idとFollowerクラスのidカラムを結びつけますよーという意味になる。
ただ、結びつけたいのはUserクラスのidカラムなので、オプションでclass_name: "User"とすることで、follower_idとUserクラスのidカラムが結びつく。
では、Userモデルのファイルにはどのように書けば良いのだろうか?
app/models/user.rbclass 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.rbclass 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.rbrequire '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.rbclass 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.rbrequire '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.rbclass 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) endinclude?メソッドは配列の要素に引数が含まれているかを判断してくれるメソッド。
余談:ダックタイピング
改良前
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 endwhen case文がいらなくなったし、workメソッドを持っているクラスであれば、work(employees)メソッドを使えるようになった。
workメソッドを持っているということが、「ガーと鳴けば」に当たるのでは?
フォロワーを考える
これはフォローの時と逆のことをすれば良い
app/models/user.rbclass 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.rbrequire '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 endUIを実装していこう
これでメソッドなどは定義できたので、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.rbRails.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] endmemberメソッドを使うと、
/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.rbRails.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.rbrequire '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 endapp/controllers/users_controller.rbclass 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.ymlone: follower: michael followed: lana two: follower: michael followed: malory three: follower: lana followed: michael four: follower: archer followed: michael作ったサンプルデータを使って統合テストを書いていく
test/integration/following_test.rbrequire '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.rbrequire '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.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create end def destroy end end次に二つのアクションの中身を実装していく。
app/controllers/relationships_controller.rbclass 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.rbclass 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.rbrequire 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.rbrequire '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.rbrequire '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.rbclass 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.rbclass 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.rbclass 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 . . . endrenderの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
- 投稿日:2019-12-09T22:03:12+09:00
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 installGem パッケージがインストールされたら、次は 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 endit に続く文字列は動詞から始める。
ここまでで 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テストの作成についてまだ知識が浅いため今回は初歩のところだけ説明しました。
知識がついたらテストの作成方法について詳しいブログを投稿します!!
- 投稿日:2019-12-09T19:57:20+09:00
はじめての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 -licensexcodeのライセンス承認をしろとのこと。
ライセンス承認後、再度上記コマンドを実行します。成功!
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.5pathを通します。
$ echo 'export PATH="~/.rbenv/shims:/usr/local/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile $ source ~/.bash_profileglobal環境を汚さないように、特定のディレクトリ下でv2.6.5を使えるようにします。
$ cd ./特定のディレクトリ $ rbenv local 2.6.5bundlerのインストール
次に、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 workspaceGemfile を生成します。下記コマンドを実行すると
Gemfile
が生成されます。$ bundle initGemfile の下記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 stopYay! You’re on Rails!
最後に、
http://localhost:3000/
へアクセスしてページが表示されたら完了です!
ruby のバージョンが2.6.5になってました!
作業時間としてはおおよそ1h程度で、非常に簡単に環境が構築できました!以上でRuby on Railsの開発環境ができましたので、いろいろ遊んでみようかと思います!
- 投稿日:2019-12-09T19:25:25+09:00
【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で代用する必要がある
- 投稿日:2019-12-09T19:22:17+09:00
Active RecordをRubyで使うための方法
概要
Active RecordをRailsではなく、Rubyから利用する機会があったので、手順についてまとめておく。
$ ruby -vruby 2.6.3p62 (2019-04-16 revision 67580)準備
今回はMySQLを利用することを前提とする。
MySQL2とActiveRecordの2種類のgemをインストールする。
gem install mysql2gem 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からも簡単に利用することができた。
- 投稿日:2019-12-09T18:53:37+09:00
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 でできる
- レスポンスして画面が表示される
- 投稿日:2019-12-09T17:38:32+09:00
Active Recordの関連付けに関する質問
railsチュートリアル2章のモデル間の関連付けについての質問です。
UserモデルとMicropostモデルの関連付けを行いました。
関係性
親 Userモデル
子 Micropostモデル
主キーはUserモデルのidで、外部キーがMicropostモデルのuser_idとなっています。それぞれのモデルで以下のように宣言しています。
micropost.rbclass Micropost < ApplicationRecord belongs_to :user enduser.rbclass User < ApplicationRecord has_many :microposts endRailsの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.useruserメソッドが定義されていない訳ではないと考えています。
二つのコードの違いは何なのでしょうか。
知識不足ですみません。教えていただけると嬉しいです。
- 投稿日:2019-12-09T17:02:15+09:00
railsでのデータの保存、更新、削除の方法 Create,Update,Delete
はじめに
今回はrailsでのデータの新規作成、更新、削除の方法をまとめていきます。
データの新規保存
newメソッド+saveメソッドかcreateメソッドでレコードを新規作成します。
newメソッド+saveメソッドの例
#モデル名.new(カラム名: データ)+モデル名.saveのセットで保存されます。 #saveメソッド忘れに注意ください。これがないと保存されません。 #以下が例です。 post = Post.new(name: "記事1", title: "hogehoge") post.savecreateメソッドの例
#モデル名.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_alldeleteメソッドの例(関連するデータは削除されません)
#モデル名.find(削除したいid).delete(カラム名: データ)でデータを削除します。 #以下が例です Post.find(1).delete #ちなみにレコード全消去したい場合は以下になります。 Post.delete_allおわりに
基本的なrailsの新規作成、更新、削除について紹介いたしました。
データ新規登録時のsaveメソッドを忘れてはまらないように注意ください!!
- 投稿日:2019-12-09T14:14:56+09:00
Rubyでグラフを描画するツール GR.rb を作ったので紹介します
これはなんの記事?
GRというグラフ描画ライブラリのRubyバインディングの記事です。
Rubyだってグラフを描きたいのです!
こんにちは。気がつくとRubyのコードをこちょこちょ書いているkojix2と申します。
Rubyでグラフを描きたいって思ったことはありませんか? もちろんRubyにもグラフを描くツールはいくつかあります。例えばNArrayの作者の田中さんが作っているnumo-gnuplot、Jupyter-labで動かすiruby-plotly, Ankaneファミリーのchartkick、かつて一世を風靡し作者が忽然と姿を消したNyaplot、フロントエンドを目指すcharty、daruと一緒に使うdaru-view。どれも良いツールではあるのですが、一長一短で私は満足できませんでした。
2019年の初め、rubyplotの開発が発表されました。これはRubyにふさわしいAPIをもつグラフ描出用ライブラリということで、非常に期待していました。しかし開発者のみなさん忙しくてなかなか時間が取れないみたいで、開発は停滞して完成しなそうな雰囲気が漂ってきました。思えばRubyにふさわしいグラフ描画のためのAPIを一から考えて実装するのは簡単にできる仕事ではなかったのだと思います。
ここ数ヶ月間、Ankaneさんが次々と機械学習のライブラリを発表されています。どうしてそんなことができるのかなと思って、プロジェクトの中身を観察してみると、ruby-ffiを利用していました。ffiを使えばC言語に詳しくなくてもバインディングを作成できるのではないかと思い、GR.rbの開発をはじめました。ネーミングは最初はffi-grだったのですが、文字が丸っこくて可愛いという理由でGR.rbを採用することにしました。
(GRではアニメーションの描出や、3Dの描出もできます! こういうのRubyではあんまり見ないでしょ?)
インストール
GRの最新のリリース版をダウンロードして解凍し、適当なディレクトリに配置します。
環境変数GRDIRを設定して、GR.rbにgrの場所を教えます。export GRDIR=/your/path/to/grgem 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)ヒストグラム
require 'numo/narray' require 'gr/plot' data = Numo::DFloat.new(10_000).rand_norm GR.histogram(data)塗りつぶした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)タイトル、軸ラベルなど
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)さらに詳しい使い方を知りたい場合は、examplesディレクトリをご覧ください。スクリーンショットに取り上げられているグラフはいずれもExampleに入っています。(APIは今後も大きく変更になる可能性があります。)
GRの中には大きく2つのモジュールがあります。GRモジュールとGR3モジュールです。
require 'gr'
とすればGRモジュールがrequire 'gr3'
とすればGR3モジュールが使えるようになります。これらは直接GRの関数を呼び出すものです。しかし、多くのケースではもっとお手軽にグラフを描出したいと思います。その場合はrequire 'gr/plot'
をすると、GRモジュールに簡単にグラフを描出するメソッドが追加・上書きされるようになります。簡単にグラフを描画するメソッドの使用例は下記のExampleを見てください。Jupyter
Jupyter + IRubyでも動作します。
そもそもGRってなに?
GRはユーリヒ総合研究機構のJosef Heinenさん達が開発しているグラフ描画ライブラリです。GRはJuliaではPlotのデフォルトのバックエンドに採用されてます。PythonやR言語など特定の言語に依存しないグラフ描画ライブラリとしてはかなり有名な方じゃないかなと思います。Githubのコントリビューションを見ると、8年前から継続的な開発が続けられているのがわかります。
工夫したところ
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さんによるPyTorch、LightGBMのバインディングの公開
- Numo NArrayが本格的に海外で知名度を獲得しはじめた
- ApacheArrowの開発が進んだ
ほかにも、4日目のアドベントカレンダーのうなぎおおとろさんがRuby用のdeep learningライブラリruby-dnnを作ってくれたりして、地味ながらも着実な成長が感じられる1年だったと思います。
来年のみなさまの生活もきっと豊かなものでありますように。
この記事は以上です。
- 投稿日:2019-12-09T14:13:40+09:00
Railsコンソールで日本語入力ができない現象【Docker】
はじめに
Railsの参考書を読みながら勉強を進めているときに、
DockerのRailsコンテナ内で、Railsコンソールを起動し、日本語文章を入力しようとしたら、
入力はできるのに、エンターキーを押すとターミナルに表示されないという現象が起きたので、その解決方法についてです。
解決方法
DockerFileに、
ENV LANG C.UTF-8
を記入して
docker-compose up --build
で再起動これでコンソールに日本語を入力できるようになります。
参考にしたページ:Docker / rails console で日本語入力できない問題
https://gist.github.com/tasiyo7333/2163a09129ed36639645145a0146d8d3
- 投稿日:2019-12-09T13:36:42+09:00
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.run1つ注意が必要なのは
Discordrb::Bot#run
はそのままだと以降の行の実行を止めてしまいます。
ですから、最後以外の#run
では引数に必ず:async
を渡しておく必要があります。動かす上での問題点
以上のようにインスタンスを分けるだけで複数のBOTを実行することができますが、残念ながら出力ログは分かてくれません。
どちらのBOTも同じようにログを出力するため、基本的に見分けることもできません。
よって実際の開発ではBOT別に十分デバッグを行った上で、運用段階で統合させるのが良いでしょう。
また、1つのアプリをデプロイするために、全てのアプリを起動しなおすことになるため、わりと手間がかかります。開発するときの工夫
実際の開発では上の例のように複数のBOTを1つのコード内に書くことはありませんし、するべきではありません。
そのため、BOTごとにクラスを定義して実行用のスクリプトで呼び出す方が現実的かと思います。
以下は私が実際に運用しているBOTで使用している方法です。test_bot1.rbrequire '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 endtest_bot2.rbrequire '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 endexecute_bots.rbrequire './test_bot1' require './test_bot2' test_bot1 = TestBot1.new("BOTのトークン") test_bot2 = TestBot2.new("BOTのトークン") test_bot1.run(:async) test_bot2.run最後に
初めてAdvent Calendarというものに参加させていただきました。
優しい日本語を心がけようとした結果、少しおかしな読みにくい文章になってしまいましたが、最後まで読んで頂きありがとうございます。
何かあればお気軽にコメントを頂ければと思います。
それでは。
- 投稿日:2019-12-09T13:15:01+09:00
Rubyとseleniumでブラウザを操作してCVテストを自動化してみた
CVテストの半自動化を試みた
同じ入力項目のLPが何十個もあるサイトで大量のCVテストが辛いのでRubyとseleniumを使って半自動化してみました。正常系だけでも半自動で確認できたらだいぶ楽かと思います。スクショも自動で取るように設定できるので勝手にエビデンスも残してくれます。
seleniumのインストール
Rubyがインストール済だったらseleniumのインストールは下記コマンドでOKです
gem install selenium-webdriverChromeDriver のインストール
which rubyでルビーがインストールされている場所を確認します。
$ which ruby /c/Ruby24-x64/bin/rubyChromeDriverを https://chromedriver.chromium.org/downloads よりダウンロードします。
ChromeDriverのバージョンがchromeと合ってないと正常に動作しないので注意が必要です。
ダウンロードしたファイル(chromedriver.exe)をwhich ruby
で確認できたディレクトリに置きます。サンプルコード
test.rbrequire '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で動的に切り替わるフォームについても実装は(可能ですが)結構キツイように思います。
- 投稿日:2019-12-09T12:46:24+09:00
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テーブルとpalcesが1対1。eventsテーブルとtagsテーブルが1対多の関係にあります。
前準備
baseにアクセスするには、アカウント自身のapi keyと base自身のapi idが必要です。下記からコピーしてください。
基本的な操作
- アカウント自身のapi key を使ってAirtableにアクセスするclientを作る
- clientをに対して、 base自身のapi id と table名 を使ってtableを指定
- 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)新規レコードが書き込まれました。
関連レコードを参照する
Airtableも魅力のひとつは関連レコードを容易に書き込めることです。
一手間かかりますが、書き込まれた関連レコードもAPIをつかうことで参照することができます。事例は、eventのタグ付けように用意したtagsテーブルのデータを読み込む方法です。
まず、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>以上。
- 投稿日:2019-12-09T12:46:24+09:00
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テーブルとpalcesが1対1。eventsテーブルとtagsテーブルが1対多の関係にあります。
前準備
baseにアクセスするには、アカウント自身のapi keyと base自身のapi idが必要です。下記からコピーしてください。
基本的な操作
- アカウント自身のapi key を使ってAirtableにアクセスするclientを作る
- clientをに対して、 base自身のapi id と table名 を使ってtableを指定
- 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)新規レコードが書き込まれました。
関連レコードを参照する
Airtableも魅力のひとつは関連レコードを容易に書き込めることです。
一手間かかりますが、書き込まれた関連レコードもAPIをつかうことで参照することができます。事例は、eventのタグ付けように用意したtagsテーブルのデータを読み込む方法です。
まず、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もっと流行るといいなー。
- 投稿日:2019-12-09T12:21:16+09:00
2億件の本番データを無事に変換した話
この記事はクラウドワークス Advent Calendar 2019 の11日目の記事です。
昨日は@lemtosh469による「アジャイル開発のスケーリングの方法」でした。
はじめに
みなさんこんにちは、クラウドワークスでサーバーサイドエンジニアをしている@shimopataです。普段はRailsを中心に機能開発したり改修作業とかに勤しんでいます。
先日、大量のデータ変換を伴うテーブルの差し替え作業をしました。データ量も多く、大変な作業でしたが計画、検証からやらせてもらい貴重な経験だったので、公開できる範囲で共有させて頂こうと思います。やりたいこと
あるテーブルに保存されているデータの形式を変換し保存し直す。
対象は以下のテーブル
テーブル名 レコード数 テーブルA 65,000件 テーブルB 4,300,000件 テーブルC 200,000,000件 2億件もレコードあるのかー
やったこと
調査と方針決め
まずはじめに、対象のテーブルがどのように使用されているのかについて調べました。一度登録されたデータがユーザの操作等によって更新されるかどうかで、対応方針が大きく異なると思い、調査し始めました。
結果、一度登録されたデータはユーザーによって、変更・削除されないことが分かり、以下の計画で変換することにしました。【計画】
1. 変換後の値を保存するテーブルを作成
2. データを変換するスクリプトを実行し、1で作成したテーブルに保存
3. テーブル名をリネームして差し替える
4. 不要なモデル、テーブルを削除メリットとしては次の通り、安心安全にデータの変換が行えるのでこの方式を採用しました。
- 書き込みを行うテーブルはユーザが直接触るテーブルではないため、書き込み時にテーブルをロックしてもユーザ影響はなく、日中帯に作業ができる。
- 変換前のデータがテーブルに残るので何かあっても、そこからリカバリーができる。
スクリプトの作成について
データ件数が多く、処理時間が長くなってしまうことから、少しでも短くするために以下の対応を行いました。
find_in_batches
を使用して、変換速度の底上げ繰り返し処理をする場合、データ数が少なければ、
each
などを事足りるのですが、今回は、データ数が多いのでfind_in_batchesを使用しました。find_eachとの違いは
- find_eachはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し、一件ずつをeachで処理していく
- find_in_batchesはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し,1配列毎に処理していく
どのくらい違いが出るかを手元にあるテストデータで試してみます。
find_eachBenchmark.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.006721000012476find_in_batchesBenchmark.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.41866779996780682000件程度のデータで約二倍の速度の違いが出ていますね。対象となるデータはこの1000000倍なので、
find_in_batches
を使うしかないですね。また、バッチサイズのサイジングもこのスクリプトのキーポイントでした。この数値を大きくする事で一度に処理できるデータの数を増やすことができ、その分、速度が早くなるのですが、当然、メモリを多く使用してしまいます。安全策をとるのであれば、夜に少しづつ変換を進めていくこともできたのですが、作業締め切りもあるため日中帯にスクリプトを流す必要がありました。
速度を出すために、バッチサイズを大きくしたい、でも、サービスに負荷をかけれない。。。
そのため、検証環境に大量のダミーデータを用意し、検証を行いました。バッチサイズの大きさを変更し実行しては、AWSコンソールメトリクスを確認を繰り返し、最適値を探しました。速度面、リソースの安全性からバッチサイズは5000としました。
activerecord-importを使用して書き込み速度の底上げ
activerecord-importというgemを使用しました。これは、一括でINSERT処理やUPDATE処理を行えるようにするgemで、スクリプト実行時間の大幅な短縮をすることができました。
使い方としては次のような感じです。sample.rbbooks = [] 10.times do |i| books << Book.new(name: "book #{i}") end Book.import booksmodelに
import
というメソッドが追加されるので、引数に取り込みたいデータを入れてあげるだけで、簡単にINSERT処理が行えます。また、オプションもたくさんあるため(特定のカラムだけ取り込む、取り込み時にバリデーションを張るなど)、興味があったら、gemのREADMEをみてみてください。こちらも、どのくらい違いが出るかを手元にあるテストデータで試してみます。
sample_bulk_import.rbusers = [] Benchmark.realtime do 1000.times do |i| users << TestUser.new(id: i,name: "test") end TestUser.import users end ------------以下実行結果----------------- 0.1599934000405483sample_import.rbBenchmark.realtime do 1000.times do |i| TestUser.create!(name: "test") end end ------------以下実行結果----------------- 5.3943878000136465圧倒的に早そうですね。
変換スクリプトの実施
事前検証も終え、負荷なども問題ないことを確認した上で、本番サーバーに対して変換作業を行いました。いくら使用していないテーブルとはいえ、本番環境に対する変更のため、二週の間、対象となるデータを約1000万件単位に分割し勤務時間帯にのみスクリプトを実行していたのですが、とにかくデータ量が多く、時間がかかりました。リソースの監視はAWSのマネジメントコンソールの情報を中心に確認していました。
いつ、どのような不具合が発生するのか分からないという状況で長時間作業していくのはなかなか体力と精神を削られる作業で大変でした。
テーブルの差し替え
変換作業が完了後、テーブル名を変換するmigrationを作成し、テーブルの差し替えを行いました。
xxxx_rename_table.rbdef 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が、なんとかこれも対応してもらい、ほんと、ありがとうございました。
データ変換をやり始めてからでないと分からないリスクとして、
- バリデーションなどの制約から外れているデータ
- 過去に何らかの理由で手動でデータ修正をして、想定していない形で入っているデータ
などを想定していたのですが、実際にやってみると意外に該当するものは少なく、よかったです。
振り返ってみるとやっていること自体は単純だったりするのですが、やっていた当時は「本当に大丈夫なんだろうか、問題ないのだろうか」とリスクがないことを証明する悪魔の証明をずっとしながら、作業工程を組み立てていたので、非常に不安になりながら作業を進めていました。
そんな状況の中でも、相談に乗ってくれたり、作業に協力してくれたエンジニアに支えられてなんとかゴールまで行けたのかなと思います。ゴールに辿り着くまでに色々失敗をしたりしたのですが、学びとして得られるものも大きく、今後に生かしていきたいと思います。
最初の調査の時に気づけなかったのは反省です。。。 ↩
- 投稿日:2019-12-09T11:48:43+09:00
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
参考文献
- 投稿日:2019-12-09T11:47:09+09:00
serverspecを導入する
Linux環境にserverspecを導入していく。
Linux環境の構築
Linux環境を構築しておく。
https://qiita.com/mkuser9/items/079cc4244821c8e220c2rubyをインストールする
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 rehashGemfileの作成
Gemfileを作成して、
gem
を追加する。console$ sudo vi GemfileGemfilesource "https://rubygems.org" gem 'serverspec' gem 'rake'
console$ bundle installserverspecのインストール
以下のコマンドで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
に生成されているので、テストの内容に合わせてテストファイルを修正する。consolevi 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
- 投稿日:2019-12-09T11:47:09+09:00
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 rehashGemfileの作成
console$ sudo vi GemfileGemfilesource "https://rubygems.org" gem 'serverspec' gem 'rake'
console$ bundle installserverspecのインストール
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テスト内容を記述
consolevi spec/localhost/sample_spec.rb
sample_spec.rb
にはすでにサンプルテストが書き込まれているため、適当に書き直す。sample_spec.rbrequire '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
- 投稿日:2019-12-09T09:39:21+09:00
#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
- 投稿日:2019-12-09T09:39:20+09:00
#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 # => ClassOriginal by Github issue
- 投稿日:2019-12-09T08:22:22+09:00
サンプルコードでわかる!Ruby 2.7の新機能・パターンマッチ(前編)
はじめに
Rubyは毎年12月25日にアップデートされます。
Ruby 2.7については2019年11月23日にpreview3がリリースされました。この記事ではRuby 2.7で導入される変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。
ただし、Ruby 2.7は多くの新機能や変更点があり、1つの記事に収まらないのでいくつかの記事に分けて書いていきます。
本記事で紹介するのはパターンマッチ(もしくはパターンマッチング)です。前編と後編にわかれています
パターンマッチは説明する内容が多いので、次のように前編と後編の2部構成になっています。
- 前編 = パターンマッチの概要、case文っぽい使い方、配列やハッシュとのマッチ、変数への代入
- 後編 = 自作クラスをパターンマッチで活用する方法、パターン名の整理
パターンマッチ以外のRuby 2.7の新機能はこちら
Ruby 2.7ではパターンマッチ以外にもさまざまな新機能や変更点があります。
それらについては以下の記事で紹介しています。こちらもあわせてご覧ください。本記事の情報源
本記事は以下のような情報源をベースにして、記事を執筆しています。
(下記の情報源はいずれもRubyにおけるパターンマッチの提唱者・開発者である@k_tsjさんによるものです)
- [RubyConf2019]Pattern matching - New feature in Ruby 2.7 - Speaker Deck
- [JA] Pattern matching - New feature in Ruby 2.7 / Kazuki Tsujimoto @k_tsj - YouTube
動作確認した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のコードが動かなくなる可能性があります。
そのため、case
やin
といった既存の予約語を活用するなどして、後方互換性に十分配慮した設計になっています。また、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 #=> onewhenが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は不要 endStep 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}"
のn
に5
が代入されています。
それなら、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今度は
n
に100
が代入されました!では、そろそろちゃんと上のコードの意味を説明しましょう。
上のパターンマッチで使われているin [3, 4, n]
の意味は次のように解釈してください。
- 要素が3つの配列で、最初の2つが
3
と4
であれば、マッチしたものと見なす- 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"}]} end2つ以上の
=>
を同時に使うこともできます。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] # ... endcase {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日に公開予定です!
- 投稿日:2019-12-09T01:19:11+09:00
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
コマンドが用意されています。細かいところで違いはあるものの、ほとんどrails
がhanami
になっただけです。プロジェクトの作成
bundle exec hanami new . --database=mysqlHanami console
bundle exec hanami consoleディレクトリ構造
上記のコマンドを実行した後のディレクトリ構造はこんな感じです。
今回、環境にdocker-composeを利用しています。dbディレクトリとDocker関係のファイルは気にしないで下さい。
apps配下にRailsでも馴染みがある
controllers
、templates
、views
が存在します。そして、Railsと大きく違う点は、lib配下にDDDらしさが伺える
entities
とrepositories
が存在する点です。簡単に説明すると、この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.rbGenerater
これは、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.rbRouting
Railsと全く同じに書けます。詳細に調べて行くと、セッションの有無に応じてルーティングを行うような高度なルーティングも出来ますが、Railsと同じ感覚でこのあたりさえ覚えておけば、ほとんど使えてしまいそうです。
apps/web/config/route.rbget '/', to: "users#show" post '/new', to: "users"または
apps/web/config/route.rbresources :users, only: [:show, :create]Model
まずは、hanamiコマンドでModelを作成します。以下のコマンドを実行すると、
entities
、repositories
が作成されます。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.rbMigration
そして、マイグレーションファイルもRailsとそれほど変わりません。
db/migrations/20191207110038_create_users.rbHanami::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 prepareEntity
特殊な要件がなければ、これだけで使用出来ます。RailsのModelと同じように、
Hanami::Entity
を継承しているだけですが、面白い違いがあります。class User < Hanami::Entity endHanamiの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を発行するのもここが担います。
create
、update
、delete
、all
、find
などの基本メソッドは、Hanami::Repository
を継承するだけで予めサポートされます。他に必要なメソッドは以下のように自作する必要があります。また、ここに複雑なロジックを噛ませることも可能です。しかし、ここにトリッキーなメソッドを定義するのは自殺行為なのでやめた方がいいです。ここでバグを仕込むとDBに保存されるデータが汚染され、恐ろしいことになるので、ロジックは出来るだけ別階層に書くべきです。遊びで試したところ、思い切ったクエリも実行出来るので下手なことをすると地獄を見そうで怖いです。
lib/src/repositories/user_repository.rbclass 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.rbmodule 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.rbmodule 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 :userInteractor
最後に、一番馴染みがない名前が登場します。この用語の解説を始めると、ドメイン駆動設計の話やクリーンアーキテクチャの話に広がってしまうため、今回は省略します(私も勉強中のため、説明出来るほど理解が進んでいない。。)。
そこで、ここでは、RepositoryとControllerの間に入る層だと思えば、少し理解しやすいと思います。
また、Repositoryの章で複雑なロジックは、Repositoryクラスに書かないように注意を促しましたが、HanamiのビジネスロジックはこのInteractorに書くことになります。
例えば、Controllerの章で例にしたActionのロジックを移設するとこんな感じです。
lib/bookshelf/interactors/user/show.rbrequire '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.rbmodule 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
- 投稿日:2019-12-09T01:07:24+09:00
C - HonestOrUnkind2
感想
今日出たABC147は2問しか解けなかった
解けなさすぎて涙出てくるホント
bit全探索だってさ。なにそれ
あと配列の個数を死ぬほど間違えて死にたくなる問題
回答
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結果