- 投稿日:2020-12-18T23:16:54+09:00
[必見!!!] オブジェクト指向についてまとめてみた!
はじめに
初学者の方にとってオブジェクト指向は参考書でも度々目にするが、そのまま調べずに終わっていませんか?
また、オブジェクト指向わかった気になっている方もいると思います。今回はオブジェクト指向について分かりやすくまとめてみましたのでご覧ください!
この記事のターゲット
- プログラミング初学者
- オブジェクト指向のプログラミング言語を学習している方(Ruby,JAVA,C++など)
オブジェクト指向プログラミングとは
「モノ」を組み立てるように表現して、コンピュータに動作をさせる
現在ではオブジェクト指向言語が主流になっていますので、プログラミングをする上でオブジェクト指向の概念を押さえておくことは非常に重要です。
たとえば、以下のプログラミング言語はすべてオブジェクト指向言語です。
- さまざまなプラットフォームで動作するJava
- Webページに動きを与えるRubyやPHPやJavaScript
- Mac OSやiOSのアプリ開発で使われるSwift
- 商用のプログラミング言語として定評のあるC++
- WordやExcelのマクロとして使用されているVBA
分かりやすく説明すると.....
オブジェクト指向プログラムとは、
モノと操作を分けて完成(アプリ)を作り上げること例えると
オブジェクト(モノ) →リモコン
操作 →ボタン、電池みたいな感じです!笑
メリット
効率よくプログラムを設計、開発できる
webサプリのボタンを例にすると、
モノと定義したボタンの場合、同じようなボタンがあった場合、ボタンの色を変えれば、他の削除や編集ボタンを作ることができる。不具合の原因を特定しやすくなる
モノと操作にオブジェクト思考で分かれているので、どこでエラーが起こったのか(モノのコード)、
どんな操作をした時か(操作のコード)がわかりやすくエラーが特定しやすい。プログラムの使用が変わっても対応しやすい
例えば、100個のボタンがあり、ボタンの幅を400px→300pxに変えたとします。100個のボタンを修正するのはだいぶ手間になる。
しかし、オブジェクト指向で定義した「ボタン」の横幅を変更するだけで、全てのボタンのサイズが変わるオブジェクト指向を使えば、開発した後も時間をかけずに修正することができます。
覚えておきたいキーワード
1.カプセル化
→他のプログラムからできるだけ変更できないようにする仕組み
ボタンを例にすると、全ボタンで高さと横幅を変えたくないとすると、他のプログラムから簡単に設定して守るのが、カプセル化です。2.継承
同じようなプログラムを1箇所にまとめてコードを再利用する仕組み
→似たような処理を一つずつ書いていると、例えば「データを登録する処理」のコードを修正することにより時間がかかってしまいます。
継承を使えば、登録、更新、削除などのカテゴリーとしてコードをまとめておける
カテゴリーとしてまとめておけば、新しい機能を作るときに同じような処理を使うことも可能。似たような処理がないか確認もしやすくなる。3.抽象化
重要な要素や共通な要素を抜き出して、他は切り捨てる考え方
→車に例えると
どんなサイズ?どんなデザイン?などの聞かなければわからないことではなく、
エンジンがついている。ハンドルが4つついているなどの必ず必要な共通要素
何が言いたいかというと、「詳しいことは決まってないけど、開発するうえで必ず必要な要素は○○だな」と捉える。4.ポリモーフィズム
継承したコードの一部を変更して利用するための仕組み
コードの一部のみを変更して利用できるのがポリモーフィズム
→例えばボタンの色だけを変えて登録、更新、削除のボタンを作ったことが、一部を変更し継承するという意味になります。終わりに
もし、オブジェクト指向を理解せずにプログラミング学習していた人も、この記事を読んであの部分がそうだったのかと気づくはずです!
皆様のご参考になれば嬉しいです!参考記事
- 投稿日:2020-12-18T23:16:54+09:00
[必見!!!] オブジェクト指向についてまとめてみた!
はじめに
初学者の方にとってオブジェクト指向は参考書でも度々目にするが、そのまま調べずに終わっていませんか?
また、オブジェクト指向わかった気になっている方もいると思います。今回はオブジェクト指向について分かりやすくまとめてみましたのでご覧ください!
この記事のターゲット
- プログラミング初学者
- オブジェクト指向のプログラミング言語を学習している方(Ruby,JAVA,C++など)
オブジェクト指向プログラミングとは
「モノ」を組み立てるように表現して、コンピュータに動作をさせる
現在ではオブジェクト指向言語が主流になっていますので、プログラミングをする上でオブジェクト指向の概念を押さえておくことは非常に重要です。
たとえば、以下のプログラミング言語はすべてオブジェクト指向言語です。
- さまざまなプラットフォームで動作するJava
- Webページに動きを与えるRubyやPHPやJavaScript
- Mac OSやiOSのアプリ開発で使われるSwift
- 商用のプログラミング言語として定評のあるC++
- WordやExcelのマクロとして使用されているVBA
分かりやすく説明すると.....
オブジェクト指向プログラムとは、
モノと操作を分けて完成(アプリ)を作り上げること例えると
オブジェクト(モノ) →リモコン
操作 →ボタン、電池みたいな感じです!笑
メリット
効率よくプログラムを設計、開発できる
webアプリのボタンを例にすると、
モノと定義したボタンの場合、同じようなボタンがあった場合、ボタンの色を変えれば、他の削除や編集ボタンを作ることができる。不具合の原因を特定しやすくなる
モノと操作にオブジェクト思考で分かれているので、どこでエラーが起こったのか(モノのコード)、
どんな操作をした時か(操作のコード)がわかりやすくエラーが特定しやすい。プログラムの使用が変わっても対応しやすい
例えば、100個のボタンがあり、ボタンの幅を400px→300pxに変えたとします。100個のボタンを修正するのはだいぶ手間になる。
しかし、オブジェクト指向で定義した「ボタン」の横幅を変更するだけで、全てのボタンのサイズが変わるオブジェクト指向を使えば、開発した後も時間をかけずに修正することができます。
覚えておきたいキーワード
1.カプセル化
→他のプログラムからできるだけ変更できないようにする仕組み
ボタンを例にすると、全ボタンで高さと横幅を変えたくないとすると、他のプログラムから簡単に設定して守るのが、カプセル化です。2.継承
同じようなプログラムを1箇所にまとめてコードを再利用する仕組み
→似たような処理を一つずつ書いていると、例えば「データを登録する処理」のコードを修正することにより時間がかかってしまいます。
継承を使えば、登録、更新、削除などのカテゴリーとしてコードをまとめておける
カテゴリーとしてまとめておけば、新しい機能を作るときに同じような処理を使うことも可能。似たような処理がないか確認もしやすくなる。3.抽象化
重要な要素や共通な要素を抜き出して、他は切り捨てる考え方
→車に例えると
どんなサイズ?どんなデザイン?などの聞かなければわからないことではなく、
エンジンがついている。ハンドルが4つついているなどの必ず必要な共通要素
何が言いたいかというと、「詳しいことは決まってないけど、開発するうえで必ず必要な要素は○○だな」と捉える。4.ポリモーフィズム
継承したコードの一部を変更して利用するための仕組み
コードの一部のみを変更して利用できるのがポリモーフィズム
→例えばボタンの色だけを変えて登録、更新、削除のボタンを作ったことが、一部を変更し継承するという意味になります。終わりに
もし、オブジェクト指向を理解せずにプログラミング学習していた人も、この記事を読んであの部分がそうだったのかと気づくはずです!
皆様のご参考になれば嬉しいです!参考記事
- 投稿日:2020-12-18T22:44:06+09:00
Action Cableでリアルタイムコメント機能(1/2)
★Action Cableでリアルタイムコメント機能を実装する
新たにチャットルームは作成せずに投稿に紐づいたコメント機能として
リアルタイムでコメントの投稿、削除が可能な実装を行いました。今回はこの実装のベースとなるコメント機能の基礎を実装します!
Action Cableでリアルタイムコメント機能(2/2)へ★実装に必要な基礎知識
Action Cableとは
通常のRailsのアプリケーションと同様の記述で、即時更新機能を実装できるフレームワークです。実装内容としては、メッセージの保存や送信に必要なRubyのコーディングと、保存したメッセージを即時に表示させるJavaScriptのコーディングです。Channelとは
チャネルとは、即時更新機能を実現するサーバー側の仕組みのことをいいます。
上記に示した通り、データの経路を設定したり、送られてきたデータをクライアントの画面上に表示させたりします。Stream_fromとは、サーバーとクライアントを関連付けるメソッドです。Action Cableにあらかじめ用意されています。
broadcastとは、サーバーから送られるデータの経路のことを指します。
broadcastを介してデータをクライアントに送信します。Clientや
Channel(.js/.rb)
Server ※イメージ
★コメント機能の基礎を実装
❶routingの記述を行う
アクションはcreateとdestroyを使用する
投稿に紐づかせたいのでpostsとネストしてあげる#routes.rb Rails.application.routes.draw do devise_for :users, controllers: { registrations: 'users/registrations' } root to: "posts#index" resources :users resources :posts do resources :comments, only: [:create, :destroy] end end❷controllerの記述を行う
投稿ページにコメントを表示させる為、comments_controllerには
コメントのcreateとdestoryに関する記述のみをする#comments_controller.rb class CommentsController < ApplicationController def create @post = Post.find(params[:post_id]) #投稿に紐づいたコメントを作成 @comment = @post.comments.build(comment_params) @comment.user_id = current_user.id if @comment.save ActionCable.server.broadcast 'message_channel', content: @comment, user: @comment.user, date: @comment.created_at.to_s(:datetime_jp), id: @comment.id,post: @comment.post end end def destroy @post = Post.find(params[:post_id]) @comment = Comment.find(params[:id]) if @comment.destroy ActionCable.server.broadcast 'delete_channel', id: @comment.id end end private def comment_params params.require(:comment).permit(:content, :post_id, :user_id).merge(user_id: current_user.id) end endposts_controllerにはコメントを表示させる為の記述を行う
今回は投稿に詳細ページを実装しているのでアクションはshow#posts_controller def show @post = Post.find(params[:id]) @comment = Comment.new @comments = @post.comments end❸viewの記述を行う
renderを利用してコメントの表示部分とコメントの投稿部分を呼び出す
#show.html.erb <div class="row"> <ul> <li class="comment-create"> <h3 class="text-left title">トークルーム</h3> </li> <li class="comments_area"> <%= render partial: 'comments/index', locals: { comments: @comments } %> </li> </ul> <% if user_signed_in? %> <div id="comment-create"> <h3 class="text-left">コメントを投稿</h3> <%= render partial: 'comments/form', locals: { comment: @comment, post: @post } %> </div> <% end %> </div>コメント表示部分
#_index.html.erb <% @comments.each do |comment| %> <% unless comment.id.nil? %> <li class="comment-container" id="test-<%=comment.id%>"> <div class="comment-box"> <div class="comment"> <div class="comment-nickname"> <p><%= link_to "@#{comment.user.nickname}", user_path(comment.user.id) %></p> </div> <div id="comment-entry"> <span style="font-weight:bold;"><%= comment.content %></span> <%= comment.created_at.to_s(:datetime_jp) %> <% if comment.user == current_user %> <%= link_to post_comment_path(comment.post_id, comment.id), method: :delete, remote: true do %> <button id="<%=comment.id%>" id="delete-btn">削除</button> <% end %> <% end %> </div> </div> </div> </li> <% end %> <% end %> <!-- コメント内容(3件目以降) ------------------------------------------------------------------> <div class="collapse" id="collapseExample"> <% @comments.offset(2).each do |comment| %> <% unless comment.id.nil? %> <li class="comment-container" id="test-<%=comment.id%>"> <div class="comment-box"> <div class="comment"> <div class="comment-nickname"> <p><%= link_to "@#{comment.user.nickname}", user_path(comment.user.id) %></p> </div> <div id="comment-entry"> <span style="font-weight:bold;"><%= comment.content %></span> <%= comment.created_at.to_s(:datetime_jp) %> <% if comment.user == current_user %> <%= link_to post_comment_path(comment.post_id, comment.id), method: :delete, remote: true do %> <button id="<%=comment.id%>" id="delete-btn">削除</button> <% end %> <% end %> </div> </div> </div> </li> <% end %> <% end %> </div>コメントの投稿部分
#_form.html.erb <%= form_with(model: [@post, @comment], url: post_comments_path(@post.id) ) do |f| %> <%= f.text_area :content, class: "input-mysize" %> <%= f.submit "送信", class: "btn btn-outline-dark comment-submit float-right", id:"submit_btn" %> <% end %>modelとmigrationファイルは省略します!
★Action Cableでリアルタイムコメント機能(2/2)へ
- 投稿日:2020-12-18T22:32:00+09:00
第11回
これは講義用のまとめサイトです。見た人は是非LGTMを押しといてください。
やったこと
- class化
class化
お題
以前作ったhello.rbをclass化しましょう
classの書き方
classは
class hogehoge def fugafuga 処理内容 end end hogehoge.newというのが基本形。これに従って改良してみる
実践
class Greeter def initialize @name = gets_name puts_hello end def puts_hello puts "Hello #{@name}." end def gets_name name = ARGV[0] || 'world' return name end end Greeter.new「Greeter」クラスの中に「initialize」というメソッドがあり、initializeが入力を受け付ける「gets_name」メソッド、出力を行う「puts_hello」メソッドを順に呼び出すという塩梅。まあ至ってシンプルですね
> ruby hello_class.rb MO_data > Hello MO_datainitializeって名前にすることで、このメソッドをクラス内に記述した時にオブジェクト作成時に自動的に呼び出されるらしい
発展問題
hello.rbだけで終わると味気ないのでテキストの発展問題に挑戦。お題は「assert_equalをclass化せよ」
require 'colorize' class assert_equal def initialize assert_equal(1,1) assert_equal(1,2) assert_not_equal(1,2) assert_not_equal(1,2) end def puts_vals(expected, result) p ['expected',expected] p ['result',result] end def assert_equal(expected,result) puts_vals(expected, result) if expected == result puts 'succeeded in assert_equal'.magenta else puts 'failed in assert_equal'.cyan end end def assert_not_equal(expected,result) puts_vals(expected, result) if expected == result puts 'failed in assert_not_equal'.magenta else puts 'succeeded in assert_not_equal'.cyan end end end assert_equal = assert_equal.new #+end src これを実行すると #+begin_src ruby codes/assert_equal_class.rb:4: class/module name must be CONSTANT class assert_equalん?CONSTANTでないといけない?ドユコト???
調べるとモジュール名は大文字で始めないといけないらしいので、class名をAssert_equalに変更すると["expected", 1] ["result", 1] succeeded in assert_equal ["expected", 1] ["result", 2] failed in assert_equal ["expected", 1] ["result", 2] succeeded in assert_not_equal ["expected", 1] ["result", 2] succeeded in assert_not_equalいけたいけた
参考記事
- source ~/MasahiroOba/grad_members_20f/members/MasahiroOba/chapter11.org
- 投稿日:2020-12-18T22:28:58+09:00
roman numerals
お題
アラビア数字(arabic numerals)を受け取って,ローマ数字(roman numerals)を返すmethodを書きなさい1.
ローマ数字で使われる文字
arabic numerals roman numerals 1 I 5 V 10 X 50 L 100 C 500 D 1000 M 例
arabic numerals roman numerals 1 I 2 II 4 IV 5 V 6 VI 9 IX 10 X 11 XI 14 XIV 15 XV 19 XIX 38 XXXVIII 42 XLII 49 XLIX 51 LI 97 XCVII 99 XCIX 439 CDXXXIX 483 CDLXXXIII 499 CDXCIX 732 DCCXXXII 961 CMLXI 999 CMXCIX 1999 最終
発展
整数はInteger classです.hello classを参照して,Integer classを拡張して,
999.to_roman #=> CMXCIXと返すようにしなさい.
参照文献
- source ~/grad_members_20f/members/djj31370/c7_roman_numerals.org
"初めてのプログラミング", Chris Pine (著), 西山 伸 (翻訳),オライリー・ジャパン (2006/7/24). ↩
- 投稿日:2020-12-18T21:28:50+09:00
Serviceクラスの実行結果をServiceResponseクラスで返す
はじめに
serviceクラスの実行結果を今までは結構適当にしていました。
例えば、更新した件数を返したり、成功したかどうかをtrue,falseで返したり、まちまちでした。
GitLabでは
ServiceResponse
というクラスを作ってそれを使って実行結果のレスポンスを返していて、とても良いと思ったのでまとめます。中身
GitLabでは下記の様になっていました。
https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/service_response.rb
# frozen_string_literal: true class ServiceResponse def self.success(message: nil, payload: {}, http_status: :ok) new(status: :success, message: message, payload: payload, http_status: http_status) end def self.error(message:, payload: {}, http_status: nil) new(status: :error, message: message, payload: payload, http_status: http_status) end attr_reader :status, :message, :http_status, :payload def initialize(status:, message: nil, payload: {}, http_status: nil) self.status = status self.message = message self.payload = payload self.http_status = http_status end def success? status == :success end def error? status == :error end def errors return [] unless error? Array.wrap(message) end private attr_writer :status, :message, :http_status, :payload end使い方
成功した場合は
ServiceResponse.success(message: 'success!')をserviceクラスの実行した際の戻り値に設定して、
失敗した場合は
ServiceResponse.error(message: 'failed')をserviceクラスの実行した際の戻り値に設定します。
下記の様に、ステータスを確認できます。
response = ServiceResponse.success(message: 'success!') response.success? #=> true response.error? #=> false response.status #=> :success response.message #=> 'success!'controller側では
result = ArticleUpdateService.new(@article, params).call if result.success? redirect_to article_path(@article), notice: result.message elsif result.error? render :new, alert: result.message endみたいな形で成功した場合と失敗した場合で処理を分けることができます。
- 投稿日:2020-12-18T21:15:17+09:00
memo
コメントアウト:コメントにしたい部分を選択して「command(windowsはcontrol) + /」
- 投稿日:2020-12-18T20:59:50+09:00
フラッシュメッセージの実装
フラッシュメッセージとは
ユーザー側の入力に基づいて処理が行われた際に、その処理の結果をユーザー側にわかるように表示されるメッセージのことです。例えば、アカウント登録、アカウント情報の更新の処理などに使用されています。
実装はさほど難しいものではありませんので、仕組みをしっかり理解しておきましょう。
実装方法
本記事ではアクションコントローラーでユーザ登録機能を実装することにします。
登録フォームは下の画像のようなシンプルなフォームです。登録ボタンを押すことで新規登録のアクションであるcreateアクションが実行されます。
(フラッシュメッセージ記述済み)app/controllers/users_controller.rbclass UsersController < ApplicationController def create @user = User.new(user_params) if @user.save flash[:success] = 'ユーザー登録が完了しました' redirect_to login_path else flash.now[:danger] = 'ユーザー登録に失敗しました' render :new end end private def user_params params.require(:user).permit(:last_name, :first_name, :email, :password, :password_confirmation) end endcreateアクションの最初の行でインスタンス変数
@user
に入力した内容が渡されます。@user = User.new(user_params)補足で説明すると、
user_params
はprivateメソッドとして下の行に定義されています。private def user_params params.require(:user).permit(:last_name, :first_name, :email, :password, :password_confirmation) endこれはストロングパラメーターというもので、簡単に説明すると、
permit
の()内で括られた値以外は取得することを何人たりとも許可しないという意味になります。これは悪意のあるユーザーがユーザーが入力した以外の情報を取得することを防ぐセキュリティ対策で、Railsでは必須の知識です。インスタンス変数
@user
は条件分岐でsave
メソッドにかけられます。if @user.saveこの
save
メソッドでユーザ登録に必要な情報が正しく入力されているかを判断し、情報に問題がなければtrue
を返します。逆に、入力欄が空白であったり、確認用パスワードが入力したパスワードと違っていたりするとfalse
を返します。いよいよ本記事のトピックであるflashメッセージとのご対面です。
情報が正しく入力されていれば登録完了の分岐となります。flash[:success] = 'ユーザー登録が完了しました' redirect_to login_path
flash[:success]
に文字が格納され、リダイレクト先であるlogin_path
に「ユーザー登録が完了しました」、というメッセージが表示されます。本当にたったこれだけです!ちなみに上記の文章は一文で記述することもできます。
コードはプログラマーがコードを理解する速さが重視されるのでこちらの記述が好まれるでしょう。
(しかし、本記事の最後の節に記載するadd_flash_types
を実装しないとできませんのでご注意ください、、、。)redirect_to login_path, flash: 'ユーザー登録が完了しました'反対に、情報が正しく入力されていなかった場合、
else
以降が実行されます。flash.now[:danger] = 'ユーザー登録に失敗しました' render :newこちらはレンダー先である新規登録ページでメッセージが表示されます。
flash.nowとflashの使い分け
結論から先に行ってしまうと、
成功時(リダイレクト)にはflash
失敗時(レンダー)にはflash.now詳しく説明すると、基本的に新規登録や編集機能などでは
if
文を使って、true
の場合はredirect_to
、false
の場合はrender
が使われます。
redirect_to
とrender
には大きな違いがあります。redirect_to・・・アクションを経由して画面遷移
render・・・アクションを経由しないで画面遷移そして
flash
とflash.now
にも違いがあります。flash・・・1回目のアクションの経由では消えず、次のアクションまで表示させる。
flash.now・・・次のアクションに移行した時点で消える。
render
はアクションを経由せずページだけを表示させるため、もしflash
を使ってしまうと表示が消えるまでに2回のアクションが必要となります。
つまり、今回の例で言えば、正しい情報が入力されログインページにて、「ユーザー登録が完了しました」、というメッセージが次に遷移するログイン後のページにも元気よく、「ユーザー登録が完了しました」と表示されてしまうのです。逆に、
redireict_to
は次のアクションを経由するのでFlash.now
では表示すらされずに、結果そのメッセージを見ることは一生ないでしょう、、、。少しややこしかったかもしれませんが、まとめると
redirect_toにはflash、renderにはflash.nowを使用する!add_flash_typesでメッセージの色を変える
最後に補足で
add_flash_types
に触れておきます。
これはBootstrapに定義されているスタイルを読み込むことが可能になります。実装方法は簡単でrailsにデフォルトで用意されているapplication_contrller.rbに設定を追加するだけです。
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base add_flash_types :success, :info, :warning, :danger endこれにより、メッセージの種類に合わせてメッセージの色を変えることができます。
(successは成功した感じの柔らかい色、dangerは失敗した時の感じの刺々しい色(笑))また、後述したように、成功時のフラッシュメッセージを表示するための
redirect_to
が1行で記述できるようになります。redirect_to login_path, flash: 'ユーザー登録が完了しました'以上、フラッシュメッセージについてでした!
- 投稿日:2020-12-18T20:41:47+09:00
【Ruby】変数の命名ルールについて。定数、クラス、メソッドの命名規則も。
個人メモです。
JSでは変数の定義時に
let
,const
など宣言があるが、rubyでは特に宣言がなく、区別方法を調べてみたのでそのまとめ。
対象 命名規則 実例 変数 小文字。アンスコでつなげる product_name 定数 すべて大文字。アンスコでつなげる PLODUCT_PRICE_LIST クラス 冒頭だけ大文字。キャメル ProductCategory メソッド 小文字。アンスコでつなげる show_price_list 定数はすべて大文字なので区別しやすい。
ハイフンは使わない。
- 投稿日:2020-12-18T20:25:55+09:00
【Ruby】クラス内のselfがつくメソッドとつかないメソッドの違い。クラスメソッドとインスタンスメソッドについて。
クラスの中の関数定義時にselfがつくメソッドとselfがつかなメソッドが混在している場合がある。
それぞれに名称があり用途、呼び出し方が異なる。
目次
それぞれの要点まとめ
それぞれの関係をまとめると以下のようになる。
項目 selfあり selfなし メソッドの名称 クラスメソッド インスタンスメソッド 対象 クラス全体 インスタンスのみ メソッド名の例 self.hello1 hello2 呼び出し方の例 クラス名.hello1 クラス名.new.hello2
確認用の実例
実用性を無視してそれぞれの関数を定義し確認してみる。
class TestClass #selfありのメソッド def self.hello1 p "self hello 1" end #selfなしのメソッド def hello2 p "hello 2" end endクラス
TestClass
の中に、以下2つのメソッドがある。
self.hello1
:クラスメソッドhello2
:インスタンスメソッドそれぞれの呼び出し方は以下のようになる。
クラスメソッドの呼び出し
selfありメソッドを呼び出す方法
selfありメソッドを呼び出すTestClass.hello1 => "self hello 1" #または ::TestClass.hello1 => "self hello 1"クラス名をオブジェクトとして直接メソッドを実行すればOK。
インスタンスメソッドの呼び出し
selfなしメソッドを呼び出すためには、インスタンスを作成し、作成したインスタンスに対してメソッドを実行する必要がある。
selfなしメソッドを呼び出す#インスタンスを生成し代入 inst = TestClass.new inst.hello2 => "hello 2" #or #インスタンスを生成し直接実行 TestClass.new.hello2 => "hello 2"
クラスメソッドとインスタンスメソッドの用途の違い
クラスメソッドはすべてのインスタンスで有効なため、商品点数や人数の総合計を算出するメソッドとして用いられることが多い。
インスタンスメソッドはそのインスタンスのみで有効なため、商品の生成や人の生成などで使われることが多い。
実用的な実例(シンプルなの)
商品カテゴリ毎に商品を登録するインスタンスと、すべてのカテゴリの商品数を足し合わせた商品点数を出力するクラスメソッドを定義したコード例。
クラス定義class Product ######クラス全体 #クラス変数(初期値0で初期化) @@product_ttl_num = 0 #クラスメソッド def self.ttl_num p "商品点数: #{@@product_ttl_num}" end #####インスタンス毎 #インスタンス生成時に自動で実行するメソッド def initialize(category) @product_category= category @product_list = [] end #インスタンスメソッド(追加用) def add(name) @product_list.push name #クラス変数に1を追加 @@product_ttl_num += 1 end endクラスの定義は以上。次にインスタンスを生成する。
インスタンスの作成##インスタンスの生成(phoneカテゴリ) phone = Product.new(phone) #インスタンスメソッド実行 phone.add("iphone6s") phone.add("iphone12pro") ##インスタンスの生成(codesカテゴリ) codes = Product.new(codes) #インスタンスメソッド実行 phone.add("USB typeC") phone.add("USB3.0")phoneとcodesの2つのインスタンスを作成し、それぞれに商品を2つづつの計4つ登録。
クラスメソッドの実行#クラスメソッド実行 Product.ttl_num => "商品点数: 4"クラスメソッドを実行すると、すべてのインスタンスの商品の合計点数を得られる。
- 投稿日:2020-12-18T19:46:11+09:00
Active hashで作ったカテゴリーを用いてransackでカテゴリー検索
今回はアクティブハッシュを用いたカテゴリ検索をしたいと思います
自分は今コーヒーの感想を共有できるアプリを作ってます。
インスタのコーヒー版的な感じです。期待する動作!
このように,「地域 ラテンアメリカ、コク ほどよい」みたいにカテゴリーを選択して投稿
選択した、カテゴリがしっかり表示されてます。
名前は、グアテマラ、地域はラテンアメリカにして、検索してみて
しっかり検索結果が表示されました!!
具体的な実装方法
今回はカテゴリ選択のactive hashと言うgemと検索機能を簡単に実装できるransackと言うgemを導入します。
active hashとは、都道府県名一覧やカテゴリーなど「基本的に変更されないデータ」があったとします。基本的に変更されないデータであるため、データベースに保存する必要性はありません。一方、ビューファイルなどにそれらのデータを直接書いてしまうと、可読性に欠けます。
そのようなケースでは、ActiveHashが有用です。
都道府県名などの変更がないデータをモデルに記述し、あたかもデータベースに保存されていたデータとして取り扱うことができるようにするGemです。すなわち、都道府県名などのデータに対して、ActiveRecordのメソッドを用いることができます。
テーブルの数を無駄に増やす必要もなくなります。ransackとはシンプルな検索フォームと高度な検索フォームの作成を可能にするgemです。
まずはこれらのgemを導入しましょう。
色々なカテゴリーを設けてますが、今回は酸味を表す、acidityカテゴリーだけに着目して解説していきます。
active hashでのカテゴリーの実装(わかってる方は飛ばしてください)
acidity.rb
class Acidity < ActiveHash::Base self.data = [ { id: 2, name: 'LOW(少ない)' }, { id: 3, name: 'MEDIUM(ほどよい)' }, { id: 4, name: 'HIGH(強い)' } ] endこれが、active hashで作った カテゴリーです。
コーヒーの投稿を保存する、drinksテーブルにacidity_idを保存してます
drinks.rb
class Drink < ApplicationRecord extend ActiveHash::Associations::ActiveRecordExtensions belongs_to :user has_one :trade has_many :drink_tag_relations has_many :tags,through: :drink_tag_relations has_one_attached :image belongs_to_active_hash :region belongs_to_active_hash :body belongs_to_active_hash :acidity belongs_to_active_hash :processing with_options presence: true do validates :name validates :explain end endbelongs_to_active_hash :acidity
と記述することで、acidityとアソシエーションが組まれて、カテゴリ選択ができるようになります
```rubyextend ActiveHash::Associations::ActiveRecordExtensions
```
と記述して、moduleを取り込むことによって、 belongs_to_active_hashメソッド
が使えますdrinks/new.html.erb
<%= f.collection_select(:acidity_id,Acidity.all,:id,:name,{},{class: "こんな感じでクラスを設定できます"})%>こんな感じでカテゴリが実装できます
第一引数に、保存先のカラム名,今回はacidity_id
第二引数に、表示したい配列データを指定する、Acidity.all
第三引数に、表示する際に参照するDBのカラム名
第四引数に、実際に表示されるカラム名
Acidity.rbのnameが厳密に言えばカラムではないが、
データベースのように扱えるので、nameを指定するこれで先ほどのようなプルダウン形式のカテゴリーの選択欄が作成できました。
検索機能の実装!
ルーティングの記述
routes.rb
Rails.application.routes.draw do root to: 'drinks#index' get '/drinks/searchdrink', to: 'drinks#search_drink' resources :drinks, only: [:index,:new,:show,:create,:destroy] do collection do get 'search' end endresourcesの上に、書かないと意図しない画面に遷移させられたりするので、それより上に書きましょう!
コントローラーの記述
drinks_controller.rb
class DrinksController < ApplicationController include SessionsHelper before_action :create_searching_object,only: [:index,:search_drink] def index @user = current_user @drinks = Drink.all.order("created_at DESC") end def new @drink = DrinkTag.new end def create @drink = DrinkTag.new(drink_params) if @drink.valid? @drink.save redirect_to drinks_path else render 'new' end end def search_drink @results = @p.result end private def drink_params params.require(:drink_tag).permit(:name,:price,:explain,:image,:tag_name,:region_id,:body_id,:acidity_id,:processing_id).merge(user_id: current_user.id) end def create_searching_object @p = Drink.ransack(params[:q]) endindexアクションでは、全投稿の情報を取得しています
create_searching_objectアクションでは、キー(:q)を使って、drinksテーブルから商品情報を探しています
@p と言う名前の検索オブジェクトを生成していますindex,search_drinkアクションのみで使用するので、before_actionで限定しています
search_drinkアクションでは@pに対して、.resultとすることで検索結果を取得して、@resultに代入しています
コントローラーの処理は以上です。
検索フォームの実装
ここでは、投稿の検索フォームを実装しましょう。その際、「search_form_for」と「collection_select」という2つのメソッドを使用します。
search_form_forはransack特有の検索フォームを生成するヘルパーメソッドです。
collection_selectメソッドはDBにある情報をプルダウン形式で表示できるヘルパーメソッドです。
drinks/index.html.erb
<%= search_form_for @p, url: drinks_searchdrink_path do |f| %> <%= f.search_field :name_cont%> <%# _contはidじゃなくて文字列のときに使う%> <p>カテゴリー検索</p> <%# ベースはドリンククラスで、第二引数で%> <%= f.label '酸味'%> <%= f.collection_select :acidity_id_eq,Acidity.all,:id,:name,include_blank: '指定なし' %> <%= f.submit '検索' %> <% end %>search_form_forの引数に「@p(検索オブジェクト)」を渡すことで検索フォームを生成しています。
urlはdrink#search_drinkに飛ばしたいので、rails routeで確認してこう言う感じになりました
<%= f.collection_select :acidity_id_eq,Acidity.all,:id,:name,include_blank: '指定なし' %>第一引数 検索したいカラム名
第二引数 実際に表示したい配列データを指定する
今回で言えば、Acidity.allです。
第三引数 表示する際に参照するDBのカラム名
第四引数 実際に表示されるカラム名
オプション include_black 何も選択してないときに表示される内容、今回は「指定無し」
検索結果を表示するビューを作成
search_drinkアクションの処理が終わったら、railsのデフォルトでsearch_drink.html.erbにリダイレクトされるので、
search_drink.html.erbを作成して<h1> 検索結果 </h1> <%# 検索結果の個数で条件分岐 %> <% if @results.length !=0 %> <% @results.each do |drink| %> <div class='main'> <%# 商品一覧 %> <div class='item-contents'> <h2 class='title'></h2> <ul class='item-lists'> <%# 商品のインスタンス変数になにか入っている場合、中身のすべてを展開できるようにしましょう %> <%if drink%> <li class='list'> <%= link_to drink_path(drink.id) do %> <div class='item-img-content'> <%= image_tag drink.image , class: "item-img" if drink.image.attached? %> <%# if drink.trade%> <%# end %> </div> <div class='item-info'> <h3 class='item-name'> <%= drink.name %> </h3> <div class='item-price'> <span><%= drink.price %>円<br>(税込み)</span> <div class='star-btn'> <%# image_tag "star.png", class:"star-icon" %> <span class='star-count'>0</span> </div> </div> <div class='item-explain'> <%= drink.explain%> </div> <div> <% if drink.region %> 産地 <%= drink.region.name%> <% end %> </div> <div> <% if drink.body%> コク <%= drink.body.name %> <% end %> </div> <div> <% if drink.acidity %> 酸味 <%= drink.acidity.name%> <% end %> </div> <div> <% if drink.processing%> 加工法 <%= drink.processing.name%> <% end %> </div> </div> <% if logged_in? && current_user.id == drink.user_id %> <div class="item-delete"> <%= link_to "削除する",drink_path(drink),method: :delete %> </div> <% if drink.trade%> <%= link_to "商品を購入する", drink_trades_path(drink) %> <% end %> <% end %> </li> <%end%> </ul> </div> <%end%> </div> <% end %> <% else %> 該当する商品はありません <% end %> <br> <%= link_to 'トップページへ戻る', root_path %>と、検索結果が、@resultに入っていて、
それをeach文で、ローカル変数をdrinkにして、検索結果があるだけ表示させています以上で、Active hashで作ったカテゴリーを用いてransackでカテゴリ検索をする実装が終わりました!
- 投稿日:2020-12-18T18:46:56+09:00
railsでパラメータを付けてredirect_toする一つの方法
いろいろかっこいいやり方はあるみたい
https://blog.kozakana.net/2015/10/redirect_to-with-parameter/ですが、今回自分の状況的に(パラメータも一つだったので)
redirect_url = ENV["HOST"] + "/hoge?h=" + hash値 redirect_to redirect_urlって感じでstringで生成して事足りてしまった。
似たようなことで悩んでる人いればご参考までに。
- 投稿日:2020-12-18T17:29:11+09:00
【Ruby】is_a?の使い方とkind_of?、instance_of?との違い。型を確認し真偽値を返す方法。
要素の型を確認し、指定した型にあっていればtrue、違えばfalseを返すメソッド、
is_a?
,kind_of?
,instance_of?
とそれぞれの違いについて。目次
使い方と実例
いずれのメソッドも使い方は同じ。
・
レシーバ.メソッド(型)
レシーバが引数で指定した型に一致していればtrue, 異なればfalseを返す。
例(is_a?)arr = [1, 2, 3] arr.is_a?(Array) => true arr.is_a?(String) => false例(kind_of?)arr = [1, 2, 3] arr.kind_of?(Array) => true arr.kind_of?(String) => false例(instance_of?)arr = [1, 2, 3] arr.instance_of?(Array) => true arr.instance_of?(String) => false
▼実際の用途
if文で変数の型毎に処理を分岐する方法が考えられる。
実例def _article_items if article_items.is_a?(Hash) [ article_items.fetch('default') ] elsif article_items.is_a?(Array) article_items else [] end end
主な型の種類
要素の例 型(クラス) "string" String 100 Integer 1.23 Floot [1, 2, 3] Array 1..3 Range {:a=>1, :b=>2, :c=>3} Hash クラス Class モジュール Module
is_a?
とkind_of?
は同じ
kind_of?
はis_a?
のエイリアスなので処理は同じ。
文字数が少ないis_a?
を使う方が一般的。
is_a?
とinstance_of?
の違い
is_a?
とinstance_of?
には明確な違いがある。・
instance_of?
: レシーバのみ評価
・is_a?
: スーパークラスの型も評価スーパークラスとは、現在の要素のクラスの親クラスのこと。
is_a?
では引数にスーパークラスの型が入っている場合でもtrueを返す。実例としては以下となる。(スーパークラスの詳細については後述)
arr = [1, 2, 3] arr.is_a?(Object) => true arr.instance_of?(Object) => falseArrayのスーパークラスであるObjectを引数にいれた場合、
is_a?
はtrue、instance_of?
はfalseとなる。
スーパークラスとは?
各要素はクラスがあり、そのクラスの親クラスがスーパークラスとなる。
例えば、
- 整数1クラス(型)はInteger。
- Integerのスーパークラスは、数字全般を表すNumeric。
- Numericのスーパークラスは、データ全般を表すObject。
- Objectのスーパークラスは、識別やメソッドの指定など基本機能だけをもつBasicObject。これが最上位のクラス。
クラスの階層構造BasicObject ↓ Object ↓ String, Numeric, Module, Class,,, ↓ Integer, Floot
(参考)最上位クラスBasicObjectのスーパークラスはnilになる。BasicObject.superclass => nil
▼要素とスーパークラスの関係
要素 型(クラス) スーパークラス スーパークラスのスーパークラス "string" String Object Basic Object 100 Integer Numeric Object 1.23 Floot Numeric Object [1, 2, 3] Array Object Basic Object 1..3 Range Object Basic Object {:a=>1, :b=>2, :c=>3} Hash Object Basic Object クラス Class Object Basic Object モジュール Module Object Basic Object nil NilClass Object Basic Object
is_a?
使用時の注意点基本的な動作は
is_a?
もinstance_of?
も変わりない。ので、文字数の少ないis_a?
が使いやすい。▼注意点1
注意点は引数をスーパークラスを指定した場合もtrueになるため、全てのデータが該当するObjectやBasic Objectを指定するとtrueが返ってしまう。
obj = {:a=>1, :b=>2, :c=>3} obj.is_a?(BasicObject) => true 100.is_a?(BasicObject) => true "文字列".is_a?(BasicObject) => true
▼注意点2
スーパークラスのクラス(型)を指定するものであり、クラス名を指定した場合はfalseとなる。class Aaa end ##クラスの継承 class Bbb < Aaa end ##スーパークラスのクラス名はfalse Bbb.is_a?(Aaa) => false ##型を指定すればtrueが返る Bbb.is_a?(Class) => true
- 投稿日:2020-12-18T17:29:11+09:00
【Ruby】is_a?の使い方。kind_of?やinstance_of?との違い。型を確認し真偽値を返す方法。
要素の型を確認し、指定した型にあっていればtrue、違えばfalseを返すメソッド、
is_a?
,kind_of?
,instance_of?
とそれぞれの違いについて。目次
使い方と実例
いずれのメソッドも使い方は同じ。
・
レシーバ.メソッド(型)
レシーバが引数で指定した型に一致していればtrue, 異なればfalseを返す。
例(is_a?)arr = [1, 2, 3] arr.is_a?(Array) => true arr.is_a?(String) => false例(kind_of?)arr = [1, 2, 3] arr.kind_of?(Array) => true arr.kind_of?(String) => false例(instance_of?)arr = [1, 2, 3] arr.instance_of?(Array) => true arr.instance_of?(String) => false
▼実際の用途
if文で変数の型毎に処理を分岐する方法が考えられる。
実例def _article_items if article_items.is_a?(Hash) [ article_items.fetch('default') ] elsif article_items.is_a?(Array) article_items else [] end end
主な型の種類
要素の例 型(クラス) "string" String 100 Integer 1.23 Floot [1, 2, 3] Array 1..3 Range {:a=>1, :b=>2, :c=>3} Hash クラス Class モジュール Module
is_a?
とkind_of?
は同じ
kind_of?
はis_a?
のエイリアスなので処理は同じ。
文字数が少ないis_a?
を使う方が一般的。
is_a?
とinstance_of?
の違い
is_a?
とinstance_of?
には明確な違いがある。・
instance_of?
: レシーバのみ評価
・is_a?
: スーパークラスの型も評価スーパークラスとは、現在の要素のクラスの親クラスのこと。
is_a?
では引数にスーパークラスの型が入っている場合でもtrueを返す。実例としては以下となる。(スーパークラスの詳細については後述)
arr = [1, 2, 3] arr.is_a?(Object) => true arr.instance_of?(Object) => falseArrayのスーパークラスであるObjectを引数にいれた場合、
is_a?
はtrue、instance_of?
はfalseとなる。
スーパークラスとは?
各要素はクラスがあり、そのクラスの親クラスがスーパークラスとなる。
例えば、
- 整数1クラス(型)はInteger。
- Integerのスーパークラスは、数字全般を表すNumeric。
- Numericのスーパークラスは、データ全般を表すObject。
- Objectのスーパークラスは、識別やメソッドの指定など基本機能だけをもつBasicObject。これが最上位のクラス。
クラスの階層構造BasicObject ↓ Object ↓ String, Numeric, Module, Class,,, ↓ Integer, Floot
(参考)最上位クラスBasicObjectのスーパークラスはnilになる。BasicObject.superclass => nil
▼要素とスーパークラスの関係
要素 型(クラス) スーパークラス スーパークラスのスーパークラス "string" String Object Basic Object 100 Integer Numeric Object 1.23 Floot Numeric Object [1, 2, 3] Array Object Basic Object 1..3 Range Object Basic Object {:a=>1, :b=>2, :c=>3} Hash Object Basic Object クラス Class Object Basic Object モジュール Module Object Basic Object nil NilClass Object Basic Object
is_a?
使用時の注意点基本的な動作は
is_a?
もinstance_of?
も変わりない。ので、文字数の少ないis_a?
が使いやすい。▼注意点1
注意点は引数をスーパークラスを指定した場合もtrueになるため、全てのデータが該当するObjectやBasic Objectを指定するとtrueが返ってしまう。
obj = {:a=>1, :b=>2, :c=>3} obj.is_a?(BasicObject) => true 100.is_a?(BasicObject) => true "文字列".is_a?(BasicObject) => true
▼注意点2
スーパークラスのクラス(型)を指定するものであり、クラス名を指定した場合はfalseとなる。class Aaa end ##クラスの継承 class Bbb < Aaa end ##スーパークラスのクラス名はfalse Bbb.is_a?(Aaa) => false ##型を指定すればtrueが返る Bbb.is_a?(Class) => true
- 投稿日:2020-12-18T16:57:08+09:00
AWSでプッシュ通知の送る方法
AWSでプッシュ送りたいであれば、サービスは2件あります:
- SNS (Simple Notification Service)
- Pinpoint
SNS
SNSの中でも2パターンが存在してます、ユーザーを一人一人にプッシュを送信するかユーザーをTOPICに登録して、そのTOPICに送信する。
Snsの流れ
SNSでアプリをApplicationを作成して、そのApplicationにエンドポイント(ユーザーのDeviceToken)を登録して、そのエンドポイントを送信する。送信後はSNSにそのユーザーに送信出来たかどうかの確認する。一人一人のユーザーに送信したいならSNSのソフトリミットは1秒1500件を送ります。
でも1<>1の送信だけではなくてTopicにも送信出来ます。
TOPICとは?
Topicは送信先のエンドポイントです、120万エンドポイントまで登録出来るエンドポイントです。エンドポイントはSNSの中で「送信出来る場所」になります。
たとえば、このアプリは100ユーザーを使ってます、プッシュ通知を送りたいならユーザーをSNSのApplicationに登録して、一人一人に送信するですが、もっと早い方法欲しいであれば、Applicationに登録する時はその1エンドポイントをTopicにsubscribeすることです。100ユーザーをsubscribeするなら、Topicに送信する時は1リクエストで100ユーザーに送信出来ます。それは100ユーザーでも、100万ユーザーでも変わらないです!
Topicのいいところは送信速度早いとAWSのApiを1回のみです。
pinpoint
Pinpointはお話題になったサービスで、今年に日本で使えるようになりました!SNSと違って、一気に100ユーザーを登録して、一気にプッシュ出来ます!
pinpointのプッシュ流れはシンプル、セグメントを作成して、セグメント作成する時はユーザーのファイルをインポートする
ファイルのサンプル
import.csv :
ChannelType,Address,Location.Country,Demographic.Platform,Demographic.Make,User.UserId
APNS,482aba02e7da338707541bb4c4a570b0ec090b8b0001b28ae1634ee680f2cbc4,JP,iOS,Apple,255830951ユーザーのdeviceTokenとプラットフォームと他の情報で組み合わせのCSVやJsonです。
セグメント作成したあとはそのセグメントに対してキャンペーンを作成して、プッシュの設定をして(タイトルとメッセージ)と送信の設定(繰り返し送信などは出来ます)をして終わりです!
送信速度も早い、1秒2万件送信されてます。
だが、
送信率をもらえますが、誰に送信出来たか出来てないは出来ません。どこのユーザーをアンインストールしたどかも出来ません。Pinpointと言う名前なのにユーザーを特定出来ないは微妙ですね...その上、安いSNSに対しては大きなユーザー数あればPinpointは高いです。
https://aws.amazon.com/jp/pinpoint/pricing/通知 100 万件までは無料、その後は通知 100 万件あたり 1 USD。
MTA の 5,000 エンドポイントまでは無料、その後は 1,000 エンドポイントあたり 1.20 USD。なので1プッシュで200万ユーザーなら1ヶ月2400ドルになります。自分の使い方としてならPinpointは合わないです、地裁ユーザーベースでたくさんのプッシュ送りたいなら出来そうです。
SNSーPinpoint
判断する為に200万ユーザーを送りたい場合はどれぐらいかかる(送信時間と値段):
SNSとpinpointは両方いいところありますが今回の要望としては大きくな対象ユーザーがあれば、Topicは一番使いやすいです。
以上
- 投稿日:2020-12-18T16:24:06+09:00
【Ruby】レシーバの型を簡単に取得するメソッド .class
個人メモです。
レシーバのクラス(型)を参照する、
class
および、親のクラスを確認するsuperclass
また
BasicObject
についても。目次
1 .class
.class
メソッドを使うと、レシーバのクラス(型)を簡単に取得することができる。x = 100 x.class => Integerstr = "text" str.class => Stringobj = {:a=>1, :b=>2, :c=>3} obj.class => Hasharr = [1, 2, 3] arr.class => Arraynil.class => NilClass(1..4).class => Range(1.23).class => FloatClass.class => Class
Class
は名前のないクラス。メタクラスと呼ぶ。
class クラス名 end
で生成するクラスはこのClass
を持っている。class TestClass def hello msg = "test class" p "hello #{msg}" end end TestClass.class => ClassKernel.class => Module
Kernel
はすべてのクラスから参照できるメソッドを定義しているモジュール。module TestModule def helloModule p "hello Module" end end TestModule.class => Module
2 .superclass
superclassメソッドを使うとスーパークラスを参照することができる。
数値、文字列、配列、ハッシュ、nil、moduleなどの要素に対しては使えない。
class
メソッドで取得した型に対してなら使用できる。スーパークラスとは?
親クラスのことをスーパークラスと呼ぶ。
class AAA end #クラスAAAを継承 class BBB < AAA end上記のようにクラスを継承した場合、親となるクラスAAAがスーパークラスとなる。
BBBをサブクラスと呼ぶ。
・
class サブクラス名 < スーパークラス名
Numericの場合
100.class => Integer 100.class.superclass => Numeric 100.class.superclass.superclass => Object 100.class.superclass.superclass.superclass => BasicObject 100.class.superclass.superclass.superclass.superclass => nilIntegerのsuperclassはNumeric。
NumericのsuperclassはObject。#数値に使うとエラー 100.superclass NoMethodError (undefined method `superclass' for 100:Integer)
classの場合
class Aaa end Aaa.class => Class Aaa.superclass => ObjectClassのsuperclassはObjectが返る。
継承した場合
class Aaa end class Bbb < Aaa end BBB.superclass => Aaamoduleの場合
moduleに対してsuperclassは使えない。
module Xxx end Xxx.class => Module Xxx.superclass => NoMethodError (undefined method 'superclass' for Xxx:Module)
3 .BasicObject
BasicObjectとはクラスの最上位概念。
クラスの識別やメソッド呼び出しなど必要最低限の機能を持ったクラスのこと。クラスの階層構造BasicObject ↓ Object ↓ String, Numeric, Module, Class,,, ↓ Integer, FlootRubyではすべてのデータをObjectと呼び、その下に文字列のStringや数値のNumericなどのくくりがある。
上にいくほどより抽象的でシンプルな定義になる。 BasciObjectはその際たるクラス。
BasicObjectのスーパークラスはnilとなる。BasicObject.superclass => nil
- 投稿日:2020-12-18T15:27:33+09:00
railsとjsを用いてタグ付け機能を実装してみる
railsでタグ付け機能を実装して、後半ではJavaScriptで発展的なタグ付けをしましょう
今回は、このようにタグを入力できる機能と、タグを入力するたびに予測変換が下に表示される機能を実装していきたいと思います!
画像で言うと、tagの入力フォームに「酸」と打ったら、下に「酸味」って予測変換的な物が表示されています
ただ、ブラウザが賢いので、ブラウザも予測変換出しちゃってますが、、、笑下記コマンドを実行
ターミナル
% cd ~/projects % rails _6.0.0_ new tagtweet -d mysql % cd tagtweetデータベース作成
データベースを作成する前に、database.ymlに記載されているencodingの設定を変更しましょう。
config/database.yml
default: &default adapter: mysql2 # encoding: utf8mb4 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sockんで、データベース作成
ターミナル
rails db:createで
Created database 'tagtweet_development'
Created database 'tagtweet_test'が作成される
データベース設計
tweet と tagは多対多の関係なので、
中間テーブルの
tweet_tag_relationsテーブルを作成するってのがポイントモデルを作成
ターミナル
% rails g model tweet% rails g model tag% rails g model tweet_tag_relationマイグレーションを編集
db/migrate/20XXXXXXXXXXXX_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[6.0] def change create_table :tweets do |t| t.string :message, null:false # messegeカラムを追加 t.timestamps end end enddb/migrate/20XXXXXXXXXXXX_create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0] def change create_table :tags do |t| t.string :name, null:false, uniqueness: true # nameカラムを追加 t.timestamps end end end今回は、タグの名前の重複を避けるために「uniqueness: true」という制約を設定します。
db/migrate/20XXXXXXXXXXXX_create_tweet_tag_relations.rb
class CreateTweetTagRelations < ActiveRecord::Migration[6.0] def change create_table :tweet_tag_relations do |t| t.references :tweet, foreign_key: true t.references :tag, foreign_key: true t.timestamps end end endtweet_tag_relationsテーブルでは、「tweetsテーブル」と「tagsテーブル」の情報を参照するので「foreign_key: true」としています。
ターミナル
rails db:migrate格モデルのアソシエーションを組む
tweet.rb
class Tweet < ApplicationRecord has_many :tweet_tag_relations has_many :tags, through: :tweet_tag_relations endtag.rb
class Tag < ApplicationRecord has_many :tweet_tag_relations has_many :tweets, through: :tweet_tag_relations endtweet_tag_relation.rb
class TweetTagRelation < ApplicationRecord belongs_to :tweet belongs_to :tag endルーティングを設定しましょう!
routes.rb
Rails.application.routes.draw do root to: 'tweets#index' resources :tweets, only: [:new, :create] end今回のアプリの仕様
何かつぶやくと,「つぶやき(tweet)」と「タグ(tag)」が同時に保存される仕様を目指します。
このような実装をする時に便利なのがFormオブジェクトというものです。Formオブジェクト
Formオブジェクトは、1つのフォーム送信で複数のモデルを更新するときに使用するツールです。自分で定義したクラスをモデルのように扱うことができます。
このFormオブジェクトは、「ActiveModel::Model」というモジュールを読み込むことで使うことができます。ActiveModel::Model
「ActiveModel::Model」とは、Active Recordの場合と同様に「form_for」や「render」などのヘルパーメソッドを使えるようになるツールです。
また、「モデル名の調査」や「バリデーション」の機能も使えるようになります。Fromオブジェクトを導入
まずはmodelsディレクトリにtweets_tag.rbを作成しましょう
app/models/tweets_tag.rbという配置です。
tweets_tag.rb
class TweetsTag include ActiveModel::Model # include ActiveModel::Modelを記述することでFromオブジェクトを作る attr_accessor :message, :name # ゲッターとセッターの役割両方できる仮想的な属性を作成 # :nameとかt保存したいカラムを書けば、保存できるって理解でまずはok with_options presence: true do validates :message validates :name end def save tweet = Tweet.create(message: message) tag = Tag.create(name: name) TweetTagRelation.create(tweet_id: tweet.id, tag_id: tag.id) end # saveメソッド内で、格テーブルに値を保存する処理を記述 end一意性の制約はモデル単位で設ける必要があるため、tagモデルに記述しましょう。
tag.rb
class Tag < ApplicationRecord has_many :tweet_tag_relations has_many :tweets, through: :tweet_tag_relations validates :name, uniqueness: true endコントローラーを作成して編集をしましょう
ターミナル
% rails g controller tweetstweets_controller.rb
class TweetsController < ApplicationController def index @tweets = Tweet.all.order(created_at: :desc) end def new @tweet = TweetsTag.new end def create @tweet = TweetsTag.new(tweet_params) if @tweet.valid? @tweet.save return redirect_to root_path else render "new" end end private def tweet_params params.require(:tweets_tag).permit(:message, :name) end end「Formオブジェクト」に対してnewメソッドを使用しています。
Fromオブジェクトで定義したsaveメソッドを使ってる
ビューの作成
tweets/index.html.erb
<div class="header"> <div class="inner-header"> <h1 class="title"> TagTweet </h1> <li class='new-post'> <%= link_to "New Post", new_tweet_path, class:"post-btn"%> </li> </div> </div> <div class="main"> <div class="message-wrap"> <% @tweets.each do |tweet|%> <div class="message"> <p class="text"> <%= tweet.message %> </p> <ul class="tag"> <li class="tag-list"> <%tweet.tags.each do |tag| %> #<%=tag.name%> <%end%> </li> </ul> </div> <%end%> </div> </div>tweets/new.html.erb
<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %> <div class='message-form'> <div class="message-field"> <%= f.label :message, "つぶやき" %> <%= f.text_area :message, class:"input-message" %> </div> <div class="tag-field", id='tag-field'> <%= f.label :name, "タグ" %> <%= f.text_field :name, class:"input-tag" %> </div> <div id="search-result"> </div> </div> <div class="submit-post"> <%= f.submit "Send", class: "submit-btn" %> </div> <% end %>CSSは省略!!!
tweets_tag.rbを編集
tweets_tag.rb
class TweetsTag include ActiveModel::Model attr_accessor :message, :name with_options presence: true do validates :message validates :name end def save tweet = Tweet.create(message: message) tag = Tag.where(name: name).first_or_initialize tag.save TweetTagRelation.create(tweet_id: tweet.id, tag_id: tag.id) end endtag = Tag.where(name: name).first_or_initializeを解説していきます
first_or_initializeメソッドは、whereメソッドと一緒に使います。
whereメソッドは,
モデル.where(条件)のように、引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得できます。
引数の条件には、「検索対象となるカラム」を必ず含めて、条件式を記述します。
whereで検索した条件のレコードがあれば、そのレコードのインスタンスを返し、なければ新しくインスタンスを
作るメソッドですとりあえずこれでタグ付けツイートの実装が完了しました
すでにデータベースへ保存されてるタグをタグ付けしたい場合、入力の途中で入力文字と一致するタグを候補として画面上に表示できる検索機能があれば、より便利なアプリケーションになりそうです逐次検索機能を実装
逐次検索機能とは、「rails」というタグがすでにデータベースに存在する場合、「r」の文字が入力されると、「r」の文字と一致する「rails」を候補としてリアルタイムで画面上に表示するっていうよくあるやつ
プログラミングで実装するときは** インクリメンタルサーチ**って言われるらしいそれでは実装していきましょう、と言いたいところですが、
application.js
require("@rails/ujs").start() // require("turbolinks").start() //この行をコメントアウトする require("@rails/activestorage").start() require("channels")上記の行をコメントアウトしないと、jsで設定したイベントが発火しないケースがあるので、コメントアウトしとくのが無難
インクリメンタルサーチ実装の準備
tweets_controller
class TweetsController < ApplicationController # 省略 def search return nil if params[:keyword] == "" tag = Tag.where(['name LIKE ?', "%#{params[:keyword]}%"] ) render json:{ keyword: tag } endとサーチアクションを定義
LIKE句は、曖昧な文字列の検索をするときに使用するものでwhereメソッドと一緒に使います
%は空白文字列含む任意の文字列を含む
要するに、params[:keyword]で受け取った値を条件に、nameカラムにその条件が一致するか、tagテーブルで検索した物をtagに代入
それをjson形式で、keywordをキーにして、tagを値にしてjsにその結果を返す。
ルーティングを設定
routes.rb
Rails.application.routes.draw do root to: 'tweets#index' resources :tweets, only: [:index, :new, :create] do collection do get 'search' end end endルーティングをネストする (入れ子にする) ことで、この親子関係をルーティングで表すことができるようになります。
collectionとmember
collectionとmemberは、ルーティングを設定する際に使用できます。
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムできます。collectionはルーティングに:idがつかない、memberは:idがつくという違いがあります。
今回の検索機能の場合、詳細ページのような:idを指定して特定のページに行く必要が無いため、collectionを使用してルーティングを設定しましょう
tag.jsを作成しましょう
app/javascript/packsはいかにtag.jsを作成しましょう
application.js
をtag.jsを読み込むために以下のように編集しましょう
require("@rails/ujs").start() // require("turbolinks").start() require("@rails/activestorage").start() require("channels") require("./tag")ここまではしっかりカリキュラムやった皆さんなら普通に理解できるはずです、こっからカリキュラムでは説明されてないとこをガッツリ解説します!
tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { console.log("読み込み完了"); }); };location.pathnameは現在ページのURLを取得、
.matchは引数に渡された文字列のマッチング結果を返す
つまり現在tweets/newにいるときにイベント発火!documentはhtml要素全体
addEventListenerは様々なイベント処理を実行DOMContentLoadedはwebページ読み込み完了したときに
つまり、html要素全体が読み込みされたときに、イベントを実行
コンソールに「読み込み完了」と表示されたらok
タグの検索に必要な情報を取得
tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { const inputElement = document.getElementById("tweets_tag_name"); inputElement.addEventListener("keyup", () => { const keyword = document.getElementById("tweets_tag_name").value; }); }); };tweets_tag_nameというidを持ったhtml要素を取得し、InputElementに代入
** ここで注意!!**
form_withによるidの付与
tweets_tag_nameっていったidを持った要素あったっけ??
tweets/new.html.erb
<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %> <div class='message-form'> <div class="message-field"> <%= f.label :message, "つぶやき" %> <%= f.text_area :message, class:"input-message" %> </div> <div class="tag-field", id='tag-field'> <%= f.label :name, "タグ" %> <%= f.text_field :name, class:"input-tag" %> </div> <div id="search-result"> </div> </div> <div class="submit-post"> <%= f.submit "Send", class: "submit-btn" %> </div> <% end %>にも、index.html.erbにもそんなidはありません。。。。。
でもなぜ取得できるか?結論からいうと
form_withが勝手にidを付与してくれるから
詳しくいうと、例えば、
form_with model: @tweetは
tweets_controller
で
def new @tweet = TweetsTag.new endと定義されてあり、
まず、idがtweet_tagになる
そして、
drinks/new.html.erbの
<%= f.label :name, "タグ" %> <%= f.text_field :name, class:"input-tag" %>:nameが
tweet_tag にくっ付いて,tweet_tag_name
ってidが生成されます!!
「どこの誰がいったことか信じられねーよ!!」って意見ももっともなので
実際に検証ツールで form_withによってidが生成されてるかどうか調べますつぶやきをツイートするmessageの場所には
tweets_tag_messagesというidが生成されて、それが、
<%= f.text_area :message, class:"input-message" %>に付与されます。
tag付けをする場所は
tweets_tag_nameというidが生成されて、それが
<%= f.text_field :name, class:"input-tag" %>に付与されます。
form_withによってidが付与される!!!
ってことを頭に入れておいてください
これで、入力フォームが取得できました
変数keywordの中身を確認
app/javascript/packs/tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { const inputElement = document.getElementById("tweets_tag_name"); // form_withで生成されたidをもとに入力フォームそのものを取得 inputElement.addEventListener("keyup", () => { // 入力フォームからキーボードのキーが離されたときにイベント発火 const keyword = document.getElementById("tweets_tag_name").value; // .valueとすることで、入力フォームに入力された値を取り出すことができる // 実際に入力された値を取得して、keywordに入力 console.log(keyword); }); }); };ここまできたら、フォームに何か入力してみましょう。
入力した文字がコンソールに出力できていればokです。XMLHttpRequestオブジェクトを生成
packs/tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { const inputElement = document.getElementById("tweets_tag_name"); inputElement.addEventListener("keyup", () => { const keyword = document.getElementById("tweets_tag_name").value; const XHR = new XMLHttpRequest(); }) }); };const XHR = new XMLHttpRequest();は
XMLHttpRequestオブジェクトを用いてインスタンスを生成し、変数XHRに代入しましょう
非同期通信に必要なXMLHttpRequestオブジェクトを生成しましょう。
XMLHttpRequestオブジェクトを用いることで、任意のHTTPリクエストを送信できます。openメソッドを用いてリクエストを定義
tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { const inputElement = document.getElementById("tweets_tag_name"); inputElement.addEventListener("keyup", () => { const keyword = document.getElementById("tweets_tag_name").value; const XHR = new XMLHttpRequest(); XHR.open("GET", `search/?keyword=${keyword}`, true); XHR.responseType = "json"; XHR.send(); }) }); };XHR.open("GET", `search/?keyword=${keyword}`, true);openメソッドの第一引数にHTTPメソッド、第二引数にURL、第三引数には非同期通信であることを示すためにtrueを指定しましょう。
なぜこういうURLの指定になるかと言うと,
このURLはqueryパラメーターといって,http://sample.jp/?name=tanakaのように、
「?」以降に情報を綴るURLパラメーターです。
「?」以降の構造は、?<変数名>=<値>となっています。今回は:idとかでtweetsを識別する必要がないので、queryパラメーターを指定する
drinks#searchを動かしたいのに、searchがなぜURLで省略されてるのか
指定したパスの一個上のディレクトリを基準に,相対的にパスを指定できるから
例えば、今回指定したパスはsearch/keyword=hogehoge
で、一個上のディレクトリはtweetsなので、
一個上のディレクトリを勝手に補完してくれるらしい。。。。これで、Drinks#searchを動かせる
と、思ったが、
XHR.responseType = "json";を書いて、コントローラーから返却されるデータの形式にjson形式を指定しましょう
そして最後!
XHR.send();を書いて、リクエストを送信しましょう.
タグの入力フォームに何かしら入力されるたびに、railsのsearch アクションが動くといった形になってます!
サーバーサイドからのレスポンスを受け取りましょう
サーバーサイドの処理が成功したときにレスポンスとして返ってくるデータを受け取りましょう。データの受け取りには、responseプロパティを使用します。
tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { # 省略 XHR.send(); XHR.onload = () => { const tagName = XHR.response.keyword; }; }); }); };const tagName = XHR.response.keyword;は、サーバーサイドの処理が成功したときに、レスポンスとして返ってくるデータを受け取って変数tagNameに代入してます
データの受け取りにはresponseプロパティを使用します。タグを表示させる処理を記述しましょう
このように、下に順に表示させていきましょう
タグを表示させる手順は以下の4つです。
1. タグを表示させる場所を取得する
search-resultと言うid名がついた要素を取得しています
- タグ名を格納させる場所を取得する。
createElementメソッドを用いてタグを表示させるための要素を生成しています。
生成した要素に検索結果のタグ名を指定しています。
- 2の要素にタグを挿入する
2で用意した要素を1の要素に挿入しています。
それぞれinnerHTMLプロパティとappendChildメソッドを用いています。
- 2と3の処理を検索結果があるだけ繰り返す
forEachを使って、繰り返し処理を行っています
tag.js
XHR.send(); XHR.onload = () => { const tagName = XHR.response.keyword; const searchResult = document.getElementById("search-result"); tagName.forEach((tag) => { // forEachを使う理由は、railsのsearchアクション // で、検索に引っかかったタグを、複数出していく // 場合もあるので const childElement = document.createElement("div"); // 2.タグを表示させるための要素を生成してる // 名前の通り,要素を作るメソッド childElement.setAttribute("class", "child"); childElement.setAttribute("id", tag.id); // 作ったdivタグにclass,idを付与する // forEachで作られたローカル変数のtagをここで使ってる childElement.innerHTML = tag.tag_name; // <div>tagname</div> って感じ // innerHTML を使用すると、 // 中身を入れ替えたり、書き換えたり、入れたりする // 3.サーバーサイドから返ってきたtagのtag_name // をchildElementの中に入れてくイメージ searchResult.appendChild(childElement); // htmlのsearch-resultの子要素に // childElementが並んでく // ここで初めて表示していく }); }; }); }); };new.html.erb
<%= form_with model: @tweet, url: tweets_path, class:'form-wrap', local: true do |f| %> <div class='message-form'> <div class="message-field"> <%= f.label :message, "つぶやき" %> <%= f.text_area :message, class:"input-message" %> </div> <div class="tag-field", id='tag-field'> <%= f.label :name, "タグ" %> <%= f.text_field :name, class:"input-tag" %> </div> <div id="search-result"> </div> </div> <div class="submit-post"> <%= f.submit "Send", class: "submit-btn" %> </div> <% end %>で
<div id="search-result"> </div>を、
tag.js
const searchResult = document.getElementById("search-result");で取得して、上記のような処理をおこなって、何か入力するたび候補を下に表示します
クリックしたタグ名がフォームに入力されるようにしましょう
タグを表示している要素にクリックイベントを指定します。
クリックされたら、フォームにタグ名を入力して、タグを表示してう要素を削除するようにしましょうtag.js
XHR.send(); XHR.onload = () => { const tagName = XHR.response.keyword; const searchResult = document.getElementById("search-result"); tagName.forEach((tag) => { const childElement = document.createElement("div"); childElement.setAttribute("class", "child"); childElement.setAttribute("id", tag.id); childElement.innerHTML = tag.name; searchResult.appendChild(childElement); const clickElement = document.getElementById(tag.id); clickElement.addEventListener("click", () => { document.getElementById("tweets_tag_name").value = clickElement.textContent; clickElement.remove(); }); }); }; }); }); };全体像こんな感じ
const clickElement = document.getElementById(tag.id); // さっき生成したタグ入力フォームの下に順に表示されていく、予測変換の欄の要素を取得 clickElement.addEventListener("click", () => { // 取得した要素をクリックすると、イベント発火 document.getElementById("tweets_tag_name").value = clickElement.textContent; // tweets_tag_nameはform_withで入力フォームに付与されるid // 入力フォームを取得 // さらに.valueとすることで、実際に入力された // 値を取得 // clickElementはタグの名前があるので // .textContentでタグの名前を取得できる // これでタグの部分をクリックしたら、タグの名前が // フォームに入ってく clickElement.remove(); // クリックしたタグのみ消えるしかし、このままだと同じタグが何度も表示されたままになってしまいます。
この原因は、インクリメンタルサーチが行われるたびに、前回の検索結果を残したまま最新の検索結果を追加してしまうからです。
インクリメンタルサーチが行われるたびに、直前の検索結果を消すようにしましょう。直前の結果検索を消すようにしましょう
検索結果を挿入している要素のinnerHTMLプロパティに対して、空の文字列を指定することで、表示されているタグを消します。
tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { # 省略 XHR.send(); XHR.onload = () => { const tagName = XHR.response.keyword; const searchResult = document.getElementById("search-result"); searchResult.innerHTML = ""; // 検索結果を挿入してる要素のinnerHTMLプロパティに // 対して、空の文字列を指定することで、表示されてる // タグを消します // 最初にこの処理が呼び出される時は当然何もないので空文字でいいし // 2回目に呼び出された時はsearch-resultが空になる tagName.forEach((tag) => { const childElement = document.createElement("div"); childElement.setAttribute("class", "child"); childElement.setAttribute("id", tag.id); childElement.innerHTML = tag.name; searchResult.appendChild(childElement); const clickElement = document.getElementById(tag.id); clickElement.addEventListener("click", () => { document.getElementById("tweets_tag_name").value = clickElement.textContent; clickElement.remove(); }); }); }; }); }); };フォームに何も入力しなかった場合のエラーを解消する
本来、インクリメンタルサーチはフォームに何か入力された場合に動作する想定です。しかし、今回イベントに指定したkeyupは、バックスペースキーなどの「押しても文字入力されないキー」でも発火してしまいます。
その結果、検索に使用する文字列がないため、レスポンスにデータが存在せず、存在しないものをtagNameに定義しようとしているのでエラーが発生してしまいます。
レスポンスにデータが存在する場合のみ、タグを表示させる処理が行われるようにしましょう。レスポンスにデータが存在しない場合にもtagNameを定義しようとすると、XHR.responseがnullなのでエラーが発生してしまいます。レスポンスにデータが存在する場合のみ、タグを表示させる処理が行われるように修正しましょう。以下のようにif文を用いて解消します。
tag.js
if (location.pathname.match("tweets/new")){ document.addEventListener("DOMContentLoaded", () => { # 省略 XHR.send(); XHR.onload = () => { const searchResult = document.getElementById("search-result"); searchResult.innerHTML = ""; if (XHR.response) { // イベントに指定したkeyupは、バックスペースキー // などの押しても文字入力されないキーでも発火してしまう // 存在しないものをtagNameに定義するとエラーが起こる // レスポンスにデータがある場合のみタグを表示させる処理を行おう const tagName = XHR.response.keyword; tagName.forEach((tag) => { const childElement = document.createElement("div"); childElement.setAttribute("class", "child"); childElement.setAttribute("id", tag.id); childElement.innerHTML = tag.name; searchResult.appendChild(childElement); const clickElement = document.getElementById(tag.id); clickElement.addEventListener("click", () => { document.getElementById("tweets_tag_name").value = clickElement.textContent; clickElement.remove(); }); }); }; }; }); }); };これで実装完了です。お疲れ様でした。
tag.jsのコードのまとめ
if (location.pathname.match("drinks/new")){ // location.pathnameは // 現在ページのURLのパスを取得、変更 // .matchは引数に渡された文字列のマッチング結果を返す // 現在drinks/new にいる時にイベント発火 document.addEventListener("DOMContentLoaded",()=>{ // addEventListenerは様々なイベント処理を実行 // することができるメソッド // documentはhtml要素全体 // DOMContentLoaded"は // Webページ読み込みが完了した時に発動 // イベント発火する範囲広くね、、、? const inputElement = document.getElementById("tweet_tag_name") inputElement.addEventListener("keyup",()=>{ // フォームに入力して、キーボードが離されたタイミング // で順次イベント発火していく const keyword = document.getElementById("tweet_tag_name").value; // テキストボックスの入力した値を取得 const XHR = new XMLHttpRequest(); // XHLHttpRequest とはAjaxを可能にするためのオブジェクトでサーバーに // HTTPリクエストを非同期で行うことができます // インスタンスを生成して、変数に代入する XHR.open("GET",`search/?keyword=${keyword}`,true); // openはリクエストの種類を指定する // 第一引数 HTTPメソッドの指定 // 第二引数 パスの指定 // 第三引数 非同期通信のON/OFF // GETリクエストで、 // ?でパラメーターを渡せる // ?keywordはキーで、${keyword}が値 // queryパラメーターとは、http://sample.jp/?name=tanakaのように、 // 「?」以降に情報を綴るURLパラメーターです。 // 「?」以降の構造は、?<変数名>=<値>となっています。 // ?文字列とかの検索をかけたい時に使う // サーチアクションを動かしたい // drinksが省略されてる理由は // 指定したパスの一個上のディレクトリを基準に // 相対的にパスを指定できる // とりあえず、drinks#searchにリクエストを送って // 予測変換したい XHR.responseType = "json"; // コントローラーから返却されるデータの形式には // jsと相性がよく、データとして取り扱いやすい // json形式を指定してる XHR.send(); // tag.jsからサーバーサイドに送信したい // リクエストを定義できたので、 // 送信する処理を記述しましょう XHR.onload = () => { const searchResult = document.getElementById("search-result"); // 1.タグを表示させる場所である,search-resultを取得 searchResult.innerHTML = ""; // 同じタグが何度も表示されたままになってしまう // 直前の検索結果を消したい // 検索結果を挿入してる要素のinnerHTMLプロパティに // 対して、空の文字列を指定することで、表示されてる // タグを消します // 最初にこの処理が呼び出される時は当然何もないので空文字でいいし // 2回目に呼び出された時はsearch-resultが空になる if (XHR.response){ // イベントに指定したkeyupは、バックスペースキー // などの押しても文字入力されないキーでも発火してしまう // 存在しないものをtagNameに定義するとエラーが起こる // レスポンスにデータがある場合のみタグを表示させる処理を行おう const tagName = XHR.response.keyword; // サーバーサイドの処理が成功した時に // レスポンスとして返って来るデータを // 受け取って,変数に代入 // データの受け取りには // responseプロパティを使用する tagName.forEach((tag) => { // forEachを使う理由は、railsのsearchアクション // で、検索に引っかかったタグを、複数出していく // 場合もあるので const childElement = document.createElement("div"); // 2.タグを表示させるための要素を生成してる // 名前の通り,要素を作るメソッド childElement.setAttribute("class", "child"); childElement.setAttribute("id", tag.id); // 作ったdivタグにclass,idを付与する // forEachで作られたローカル変数のtagをここで使ってる childElement.innerHTML = tag.tag_name; // <div>tagname</div> って感じ // innerHTML を使用すると、 // 中身を入れ替えたり、書き換えたり、入れたりする // 3.サーバーサイドから返ってきたtagのtag_name // をchildElementの中に入れてくイメージ searchResult.appendChild(childElement); // htmlのsearch-resultの子要素に // childElementが並んでく // ここで初めて表示していく const clickElement = document.getElementById(tag.id); // クリックしたタグ名がフォームに入力されるようにしたい // 入力していったら,id = tag.idのdivのhtml要素 // ができているはずなので、それを取得 clickElement.addEventListener("click",()=>{ // clickElement要素をクリックした時にイベント発火 document.getElementById("tweet_tag_name").value = clickElement.textContent; // form_withで作られたidの要素を取得 // さらに.valueとすることで、実際に入力された // 値を取得 // clickElementはタグの名前があるので、 // .textContentでタグの名前を取得できる // これでタグの部分をクリックしたら、タグの名前が // フォームに入ってく clickElement.remove(); // クリックしたタグのみ消える }); }); }; }; }); }); };
- 投稿日:2020-12-18T14:47:41+09:00
次世代シーケンサーのデータをRubyで操作するツールを作りたい話
はじめに
こんにちは。この記事はバイオインフォマティクス Advent Calendar 2020 の記事です。
本当は、今日までにRuby-htslibの開発を進めて、公開したかったのですが、あまり進んでないので、もうあきらめて、背景的なことをタラタラと書いていこうと思います。
この記事は未完成ですが見切りをつけて公開します。ruby-htslibの開発が進んだら更新しようと思います。
Rubyに対するこだわり
R、Python, Perl, Julia, C++..世の中には星の数ほど素晴らしい言語があります。しかし私はRuby言語が好きです。私がたまたま最初に書けるようになったのはRuby言語だったからです。そこで、あたかもカルガモのヒナが最初に見たプログラミング言語を親だと思いこむように、Rubyが母国語になりました。(嘘です。Ruby言語の前にObjective-C言語とかに挑戦したことがありますが諦めました。Objective-C言語に比較すると、Rubyは単純で美しく見えました)
そんなRuby言語の特徴は、RやJulia等と比較すると、全てがオブジェクトで構成され、関数のかわりにメソッドを使うことです。この世界では「データ」と「手続き」がひとまとまりの「オブジェクト」として存在しています。これは一般的にデータ解析では不利な性質だと思います。一方で「何かを道具を作りたい」という人にとっては、力強いサポートになります。私も気がつくと、Rubyでツールを作ろうとしてしまいます。
Rubyでも次世代シーケンサーのデータを扱いたい
さて、Ruby言語はデータ解析にはあまり向いていないかもしれないと言いましたが、中には根強いファンもいて、BioRubyという生命情報用のライブラリもります。
- BioRuby: http://bioruby.org/
- リポジトリ: https://github.com/bioruby/bioruby
- ドキュメント: http://bioruby.org/rdoc/
けれども、今日の、何やらすごい研究のデータは、次世代シーケンサーの出力するデータを利用していることが多いです。そういった次世代シーケンサー関連のデータのファイル形式、つまり
SAM
,BAM
,VCF
,BCF
といったフォーマットを扱います。これを扱うのはBioRubyでは難しいらしい、ということが私にもわかってきました。誰か素敵なライブラリを作っていないのかな? と思って探しました。ひとつありました。Bioruby-samtoolsです。
- リポジトリ: https://github.com/helios/bioruby-samtools
- 論文: Bio-samtools: Ruby bindings for SAMtools, a library for accessing BAM files containing high-throughput sequence alignments
けれどもこのプロジェクトはSamtoolsをコマンドラインから呼び出すツールでして、細かくファイルを操作できるツールとはちょっと違う感じでした。(このプロジェクトは以前FFIを利用してSamtoolsのバインディングとして構成されていましたが、SamtoolsからHTSlibが分離された時にOpen3を使用するようになったそうです。Open3とは、プログラムを実行し、そのプロセスの標準入力・標準出力・標準エラー出力にパイプをつなぎます。つまりバインディングではありません。APIが安定したらFFIを利用してHTSlibのバインディングを作成する予定だったそうですが、開発者の方は余裕がなくてなかなか作成できなかったそうです)
一方で、Python、Rといった言語では次世代シーケンサーのファイルを利用できるライブラリが存在します。そして結果として解析用のtoolやパイプラインも多く開発されています。
普通であれば、目的に応じてツールを選択するということで、柔軟に言語を切り替えて利用するべきです。私も頭ではわかっているのですが、なかなかそのような合理的な方法をとることができません。モグラのように、なんとかしてRuby言語でもこれらのファイルを扱う方法はないだろうかと考えはじめました。
SAM/BAMというフォーマット
SAM
ファイルやBAM
ファイルといった形式は、リードのアラインメントを表したファイルです。次世代シーケンサーはFASTQというリードのファイルを生成します。このファイルを、bwa
やSTAR
といったアライナーでリファレンスゲノムにマッピングした結果のファイルがSAM/BAMファイルです。これは少し奇妙なファイル形式だと思います。私たちの身の回りには、SQLiteなどもっと一般的なデータベースがあります。タブ区切りテキストや、MS Excelといったファイル形式もあります。しかし生命情報の世界ではそれらのファイルではなく、SAM/BAMファイルが好んで利用されているようです。
@HD VN:1.6 SO:coordinate @SQ SN:ref LN:45 r001 99 ref 7 30 8M2I4M1D3M = 37 39 TTAGATAAAGGATACTG * r002 0 ref 9 30 3S6M1P1I4M * 0 0 AAAAGATAAGGATA * r003 0 ref 9 30 5S6M * 0 0 GCCTAAGCTAA * SA:Z:ref,29,-,6H5M,17,0; r004 0 ref 16 30 6M14N5M * 0 0 ATAGCTTCAGC * r003 2064 ref 29 17 6H5M * 0 0 TAGGC * SA:Z:ref,9,+,5S6M,30,1; r001 147 ref 37 30 9M = 7 -39 CAGCGGCAT * NM:i:1↑ 仕様書(hts-spec)に載っているSAMファイルの例。見慣れないヘッダー、フラグ、タグがついている。フラグは、リードがマッピングされているかいないかといった非常に重要な情報が含まれている。
- SAM/BAMに関連するリンク
フラグは2進数で表記されており、パット見ても何を意味しているのか人間にはわかりません。
samtools flag
コマンドを使うと、フラグの意味を表示してくれます。samtools flag 2064 # 0x810 2064 REVERSE,SUPPLEMENTARY↑ samtoolsでフラグの意味を表示したところ
Integer Binary Description 1 000000000001 ペアリードがある 2 000000000010 両方マップされている 4 000000000100 read1 はマップされていない 8 000000001000 read2(相方) はマップされていない 16 000000010000 read1 は逆相補鎖にマップされている 32 000000100000 read2(相方) は逆相補鎖にマップされてる 64 000001000000 read1 である 128 000010000000 read2 である 256 000100000000 複数個所にマップされている 512 001000000000 マッピングQVが低い 1024 010000000000 PCR or optical duplicate 2048 100000000000 supplementary alignment (e.g. aligner specific, could be a portion of a split read or a tied region) ↑ フラグの表
BAMファイルはSAMファイルの圧縮形式で、gzipと互換性のあるフォーマットだそうです。BAMファイルは圧縮されても検索しやすくなるように工夫されているようです。
CRAM
というよく似たファイル形式もあります。こちらはBAMよりも圧縮率が高いファイル形式のようです。これら特殊な形式のファイルを、Samtoolsというコマンドラインツールを使っていろいろ操作していくのです。実際のところ、この初心者に対する嫌がらせのようにも思えるファイル形式が使われている理由は、歴史的な経緯と、人類の技術的限界によるところが大きいのだと思います。RNA-SeqのBAMファイルは小さいものでも数GBあったりしますし、全ゲノムシーケンスなら数十GBにおよびます。これだけでも十分に巨大ですが、SQLiteやTSVにするとその数倍以上のサイズになるでしょう。そのような巨大なファイルをエクセルで検索することは当然難しくなります。解析するのも大変でしょうし、保存するスペースを確保するのも大変でしょう。サイズの問題、そして可能な限り高速に検索するために、特殊なフォーマットが開発されたということなのでしょう。(興味がある方は下のエントリーをお読みください)
- Forum: Why does bioinformatics use a special format like a BAM file instead of a database like SQLite?
- 煽り気味の質問にも親切に答えてくれるBiostarsのみなさん
しかしながら、コンピュータの性能は上がり続けています。いつまでも同じ状況が続くとは限りません。10年後、20年後には、現在の配列情報の処理は、スマートフォンで十分であるという時代が来るかも知れません。たとえばbiowasmとかいうプロジェクトがあります。これは、なんとWebAssemblyを使うことでブラウザ上で
samtools
bedtools
bowtie
seqkit
等を動かしてしまうというようなものだそうです。↑ ブラウザ上でゲノミクスのツールを動かす野心的なプロジェクトbiowasm
現状ではこのようなツールは役に立たないかも知れません。世間的には巨額な予算を獲得して、ワークフロー言語を使ってクラウド環境やスーパーコンピューターなど多数のPCを操作して、数の暴力により効率的に大量に検体を処理して新事実を発見しないとなかなか評価の対象にならないのかもしれません。しかしそんな勇ましいことを言っても、そんなことができるのは現実的には地球上でもごく少数の人だけでしょう。ですから、そのように物事をとらえてしまうと、どんどんと暗い気持ちになってしまうのではないでしょうかね。(少なくとも私は暗い気持ちになります)
ですから、そういった世知辛さとは遠いところにあるツールに触れたり眺めたりしている時間こそ、心あたたまる大切な時間だという気がいたします。
SAM/BAMのアイコンが見当たらない話
話がとびましたが、さらに話がとびます。
SAMファイルやBAMファイルのアイコンというものが見つからないことが気になっています。バイオインフォ用アイコンセットというものがないか結構探しました。コマンドライン上の操作に強い人はそんなの気にならないのかもしれませんが、個人的には気持ちよく作業をするためにはファイル・アイコンの存在はとても大事なことだと考えています。
もしもバイオインフォ用の素晴らしいアイコンセットを知っている方はコメント欄で教えてほしいなと思います。多分世界のどこかにはそういうものを作っている人がいるけれども、見つかってないだけだと思います。まあ自分でデザインして、身銭を切ってデザイナーの方に仕上げてもらい、それを著作権フリーで公開するという最終手段もありますが。
SAM/BAMの仕様書を読む
- SAM/BAM仕様書: https://github.com/samtools/hts-specs
は、Githubのリポジトリとして公開されています。日本語の情報があまりないので、詳しい内容を知りたい時は原典にあたる必要があります。しかし、PDFファイルであるため、Google翻訳がなかなか効きません。なので、PDFから文字起こしをして、Qiitaに記事として投稿しました。
↑のサイトをGoogle翻訳することによって、ある程度日本語でSAM/BAMファイルの仕様を把握することができます。
VCF形式とBCF形式
VCF/BCFは突然変異の形式です。こちらはまだ表面的にも理解できているとは言えないので、今後の勉強が必要です。まずはSAM/BAM形式のように仕様書を機械翻訳して全体の概要をつかもうと考えています。
Samtoolsの内部ではHTSlibが動いている
さて、このSamtoolsはC言語で作られていて、Samtoolsの内部ではhtslibというライブラリが、いわばエンジンのように動いていることがわかりました。HTSLibは、もともとSamtoolsの一部だったものをHTSLibとして切り分けたようです。そして、Python言語やJava言語など、いろいろな言語でhtslibに対するバインディングが作成されていることがわかりました。PythonやRのような言語でも、SAM/BAM/VCF/BCFを直接扱うようなツールは、htslibを間接的に利用していることを知りました。
↑ 各種言語におけるhtslibバインディング
面白いところはRustやNimといった、新しい静的型付き言語において熱心にバインディングが開発されていることです。これは、速度に対する需要が大きいのだろうなと思います。ゲノムのデータは膨大なので、Pythonのようなスクリプト言語よりも、RustやNim、Goといった言語を使って高速に処理することが求められているのでしょう。
上から5番目のJulia言語のバインディングは日本の方が開発されているようです。中をみてみますと、C言語のヘッダーファイルをパースしてバインディングをある程度自動生成しているようでした。
図:HTSLib.jl には独自のバインディング自動生成スクリプトが付属しているそこで私は考えました。Ruby言語も htslibのバインディングを作成すれば、ある程度は次世代シーケンサーのファイルが扱えるようになるのではないかと。
こういう時、PythonやR、Juliaなど流行の言語であれば、世界の素晴らしい方が作ったバインディングを探してどれを使えばいいか悩むのかも知れませんが、Rubyでは解決策は一つしかないのであれこれ悩む必要はありません。
自分で作るか、作らないかです。
HTSLibの論文が出たようです
また話がずれますが、偶然にも、HTSLibの論文が12月16日にアップロードされました。これを受けて、いろいろな言語でhtslibのバインディングの作成が活性化するかも知れません。
↑ Nim言語のバインディングhts-nimの作者が「便利そうダナー」とコメントしているツイッター
C言語のRubyのバインディングを作るということ
では、どうやってRubyのバインディングを作っていけばいいのだろうか、ということになります。方法は大きく分けて2通りあります。ネイティブ拡張を利用する方法と、FFIを利用する方法です。ネイティブ拡張を使う方法は、C言語をゴリゴリ書いていく必要があります。私はC言語を扱えませんのでこの方法を採用することはできません。もうひとつはFFIを利用する方法は簡単で、あまりC言語に詳しくなくても、Rubyバインディングを作ることができます。
私は1年前から、GR.rbというRubyでグラフを描出するツールを作っています。
↑ Rubyでもきれいなグラフや図が描出できるGR.rb
Julia言語では事実上のデファクトスタンダードとなっているGRというグラフ描出ライブラリのバインディングです。FFIはFiddleというRuby標準ライブラリを使っています。Rubyバインディングを作るのは、配線をつなぎ合わせてプラモデルを作るようなもので、そこまで高度な技術がなくてもできてしまいます。
先行している素晴らしいバインディングのコードを観察してパターンを学び、同じように作れば、ほとんどの場合うまく動作するものが作れます。このプロッティングツールも、Rubyでグラフを描出できるライブラリが足りないため、自分で作り始めたものです。
RubyコミュニティではAndrew Kaneさんという方が、2年ほど前から精力的にC言語の機械学習のツールのバインディングを作成されており、RubyでC言語のバインディングをFFIを利用して作ろうとする方は、Ankaneさんのリポジトリを観察すると、とても参考になると思います。
Rubyのhtslibバインディングを作る意味
次に、htslibのRubyバインディングを作る意義は何かという話になります。
開発する個人的なメリットとしては、ruby-htslibを作っているうちに、SAMやVCFのファイル仕様を読む必要が出てくるため、自然にフォーマットに対する知識がいろいろ身につくのではないかという気がします。また、これから先、次世代シーケンサーを超えて、さまざまな新しいモダリティが出現する時がかならず来ると思います。その場合も、いろいろなフォーマットが出現し、それを処理する基盤となるツールやライブラリが作成されて、各種言語からのバインディングが作られるという人間の営みは大きく変わりません。共通の課題に対していろいろな人々が頑張っている場を学ぶことには大きな意義がある気がします。
つぎに、Rubyよりももっとデータ解析に適した言語がいろいろ存在する中で、Ruby言語でバインディングを作る社会的な意味を考えます。これはやってみないとわかりません。RubyはActiveRecordに代表されるように、データをオブジェクトに変換して扱うという分野では使いやすさに定評があります。ですから、作り続けていくうちにHTSLibのRubyバインディングの良さのようなものが現れるとしたら、一つそういった方面があると思います。もちろんRuby言語を利用した解析ツールや、解析パイプラインのようなものが作れればそれに越したことはないですが、これはなかなか実現が難しいのではないかと考えています。いずれにせよ、プログラミング言語の世界でも多様性は大事であると考えます。
基本的な方針
さて、ここから先は、技術力がない人が、どうすればruby-htslibを作れるだろうか?という現実的な話になります。
最初のポイントはFFIのライブラリ選びです。Ruby言語では、Ruby-FFIと、Fiddleの2種類のFFIバインディング作成のためのライブラリがあります。どちらも歴史あるライブラリです。Ruby-FFIはより高機能なサードパーティのライブラリです。FiddleはRubyの標準ライブラリですが機能は簡易的で普及度はあまり高くありません。
- Ruby-FFI: https://github.com/ffi/ffi
- Fiddle: https://github.com/ruby/fiddle
今回Ruby-FFIを採用することにしました。Ruby-FFIは多くのプロジェクトで採用されており、Ruby配列とCポインタの相互変換、ネストされた構造体などを扱うのが得意です。Ruby-FFIのあまりよろしくない点としては、Ruby標準ライブラリではないため追加のライブラリのインストールが必要になることと、将来的にRubyの構成が大きく変わった時に変化についていけないリスクがあること、日本人開発者がいないため、日本語で質問しにくいという点があります。
↑まずはRuby FFIの全体像を得るために、Google翻訳を使って FFI wikiの全訳を作成しました。
さて、低レベルのバインディングでRuby-FFIを利用しても作るのが大変だというところは2点ほど思いつきます。
一つはRuby-FFIがC言語の bit field に対応していないところです。FFIを bit field に対応させましたというGemもあるのですが、公開日が古く、そのまま使うわけにはいかないと思っています。
もう一つ難しい点は、メモリ管理です。
Rubyではガーベッジコレクションが参照されなくなったオブジェクトを自動的に削除します。そのタイミングでFFIもメモリを開放するようです。しかし、何かの理由でうまく動作せず、開放するべきメモリが開放されなかったり、逆にGCが勝手にメモリを開放してしまうような場面が出現するかも知れません。そうなったとき、その問題を発見したり、デバッグしたりするのは私には大変難しいだろうなと考えています。
そういったものに当たったときには、人の助けを借りるしかないと思います。自動的にCのヘッダーファイルをパースしてバインディングを生成できないのか?
FFIのバインディングを作る場合、通常はC言語で書かれたヘッダーファイルの関数を、正規表現などを用いて置換を繰り返して、RubyのFFIモジュールに書き換えるのではないかと思います。けれども自分で一つ一つ関数をメソッドに書き換えるケースも悪くはありません。一つ一つのメソッドを手書きして実装するのは、時間と手間をかければ、誰でもやることができます。単調な作業に見えますが、実は大きなメリットがあります。それはライブラリのAPIを写経することで、ツールの全体像を深く理解できるようになることです。
けれども人間が手作業でバインディングを作成する場合、どうしても書き損じやタイポが生じます。HTSLibは現在でも開発が続いており、今後も関数は追加されたり、変更されたりします。HTSLibのバージョンが上がれば、Rubyバインディングも微修正を加えなければなりません。私は手作業は好きですし大事だと思っていますが、それでけでなく、何らかの自動化も考えていきたいところです。
一つの方針は、Julia言語のケースのように自分で変換器を作成することです。この場合、正規表現による置換を利用したり、Ruby言語で実装されたC言語のパーサを使って、バインディングを自動生成するスクリプトを作成すれば良いでしょう。もう一つは、RustのHTSlibバインディングのように、すでにある誰かが作った変換器を利用することも考えられます。
Ruby-FFIには自動的にバインディングを生成するプロジェクトがありましたが開発が事実上中断しています。最終更新日が古いのでそのまま使うことはできないでしょう。
また、次のようなプロジェクトもあるようです。こちらはうまく動作するようですので、利用できないかどうか検討しています。
nmコマンド
関連する方法として、共有ライブラリのバイナリファイルを直接観察するという考え方があります。
nm htslib.soとすると、共有ライブラリのバイナリファイルから直接関数の名前を見ることができます。
実装できる関数をなるべくカバーするという目的では、このようなバイナリを直接見る手段もどこかで役に立つかも知れません。Fat Gemの問題 共有ライブラリをパッケージに含めるべきかどうか?
共有ライブラリをパッケージ含めるメリットは、バージョンの調節が容易になることです。しかしGemのサイズが大きくなりますし、ユーザーによってはGemの中にバイナリファイルが含まれていることは好まず、自分で用意したhtslibを使いたいと考えるはずです。また、クロスプラットフォームにすることを考えた時、gemをプラットフォームごとに分割しない場合は、Windows, Mac, Linuxの3種類のバイナリを梱包しておく必要があります。(詳しくないのでもっとよい方法があるかも)
共有ライブラリを探す/指定する方法には、環境変数でパスを指定する方法のほか、Pkg-configを利用して、ファイルを見つける方法などがあります。htslibもpkg-configに対応しているっぽいです。しかしAnacondaなどでhtslibを入れた場合などの動作確認が必要と思われます。
高レベルのAPI
ffiを使ったバインディングは、2種類のAPIを作成します。
低レベルのAPIは、FFIモジュールを作成して、特異メソッドとして用意します。FFIモジュールの特異メソッドを呼び出すと、htslibの関数が呼び出されます。
高レベルのAPIでは、よりRubyらしいAPIを提供します。Rubyらしさとは何か、というのは難しい問題です。NGS関連ファイルを無理なくオブジェクト指向で扱えることが目標になると思います。まだしっかり理解できていないのでhts-pythonを移植して、それを改良することを考えています。
bamファイルを検索するような使い方では、ActiveRecordsのAPIが参考になるかもしれません。私はActiveRecordの使い方は詳しくないので、いろいろ勉強する必要があります。
高レベルのAPIを考えるためには他の言語のツールとかをよく使ってみてhtslib全体を理解する必要がありそうです。私の実力はまだまだで、ここからかなり勉強と経験が必要な気がするので、年単位で実現していけばなあと思っています。
現在の進捗状況
今の段階では、低レベルのAPIを主に手書きで作り終えたところです。
次のようなサンプルで、bamファイルを読み込むことができます。
require 'htslib' bam_path = File.expand_path('../assets/poo.sort.bam', __dir__) htf = HTS::FFI.hts_open(bam_path, 'r') idx = HTS::FFI.sam_index_load(htf, bam_path) hdr = HTS::FFI.sam_hdr_read(htf) b = HTS::FFI.bam_init1 nuc = { 1 => 'A', 2 => 'C', 4 => 'G', 8 => 'T', 15 => 'N' } cig = { 0 => :BAM_CMATCH, 1 => :BAM_CINS, 2 => :BAM_CDEL, 3 => :BAM_CREF_SKIP, 4 => :BAM_CSOFT_CLIP, 5 => :BAM_CHARD_CLIP, 6 => :BAM_CPAD, 7 => :BAM_CEQUAL, 8 => :BAM_CDIFF, 9 => :BAM_CBACK } 10.times do HTS::FFI.sam_read1(htf, hdr, b) p b[:core].members.zip(b[:core].values) p name: b[:data].read_string, flag: b[:core][:flag], pos: b[:core][:pos] + 1, mpos: b[:core][:mpos] + 1, mqual: b[:core][:qual], seq: HTS::FFI.bam_get_seq(b).read_bytes(b[:core][:l_qseq] / 2).unpack1('B*') .each_char.each_slice(4).map { |i| nuc[i.join.to_i(2)] }.join, cigar: HTS::FFI.bam_get_cigar(b).read_array_of_uint32(b[:core][:n_cigar]) .map { |i| s = format('%32d', i.to_s(2)); [s[0..27].to_i(2), cig[s[28..-1].to_i(2)]] }, quご覧のように、低レベルのAPIだけ使って無理やり表示しているのでごちゃついています。
まだまだですね…。先は長いです。これを実行すると次のように表示されます。[[:pos, 3289], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3289], [:isize, 0]] {:name=>"poo_3290_3833_2:0:0_2:0:0_119", :flag=>133, :pos=>3290, :mpos=>3290, :mqual=>0, :seq=>"GGGGCAGCTTGTTCGAAGCGTGACCCCCAAGACGTCGTCCTGACGAGCACAAACTCCCATTGAGAGTGGC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3292], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 3], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3292], [:isize, 0]] {:name=>"poo_3293_3822_1:0:0_0:0:0_76", :flag=>133, :pos=>3293, :mpos=>3293, :mqual=>0, :seq=>"TTCGATGCGGGACCCCCAAGACGTCGTCCTGACGAGCACAAACTCCCATTGAGAGTGGCACATGATTTCC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3293], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3293], [:isize, 0]] {:name=>"poo_3294_3861_2:0:0_2:0:0_2d7", :flag=>69, :pos=>3294, :mpos=>3294, :mqual=>0, :seq=>"TGGGGACCGTGTGACTATCAGAAGGGTGGGGTCAGCTTGTTCGATGCGGGACCCCCAAGACGTCGTCCTG", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3298], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3298], [:isize, 0]] {:name=>"poo_3299_3808_0:0:0_4:0:0_2e0", :flag=>69, :pos=>3299, :mpos=>3299, :mqual=>0, :seq=>"CCCAAGACGTCGACCTGAGGAGCACAAACTCCCAATGAGAGTGGCACATGATTTGCCCAACCATACCATT", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3303], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3303], [:isize, 0]] {:name=>"poo_3304_3813_0:0:0_2:0:0_2f5", :flag=>133, :pos=>3304, :mpos=>3304, :mqual=>0, :seq=>"GGACCCCCAAGACGTCGTCCTGACGCGCACAAACTCCCATTGAGAGTGGCACATTATTTCCCCAACCATA", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3311], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 3], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3311], [:isize, 0]] {:name=>"poo_3312_3825_0:0:0_1:0:0_6f", :flag=>133, :pos=>3312, :mpos=>3312, :mqual=>0, :seq=>"TTGTTCGATGCGGGACCCCCAATACGTCGTCCTGACGAGCACAAACTCCCATTGAGAGTGGCACATGATT", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3317], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3317], [:isize, 0]] {:name=>"poo_3318_3847_3:0:0_1:0:0_142", :flag=>133, :pos=>3318, :mpos=>3318, :mqual=>0, :seq=>"CTATCAGAAGGGTGGGGGCAGCTTGTTCGATGCGGGACCCCCAAGACGACGTCCTGACGAGCACAAACTC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3319], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3319], [:isize, 0]] {:name=>"poo_3320_3822_3:0:0_6:0:0_333", :flag=>69, :pos=>3320, :mpos=>3320, :mqual=>0, :seq=>"TTCGATGCGGGACCCCCAAGACGTCGTGCTGACGAGCACAACCTCGCAATGAGAGTGGCACAAGATTTGC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3321], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3321], [:isize, 0]] {:name=>"poo_3322_3847_1:0:0_0:0:0_2cb", :flag=>69, :pos=>3322, :mpos=>3322, :mqual=>0, :seq=>"CTATCAGAAGGGTGGGGGCAGCTTGTTCGATGCGGGACCCCCAAGACGTCGTCCTGACGAGCACAAACTC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3327], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3327], [:isize, 0]] {:name=>"poo_3328_3840_1:0:0_2:0:0_211", :flag=>69, :pos=>3328, :mpos=>3328, :mqual=>0, :seq=>"AAGGGTGGGGGCAGCTTGTTCGATGCGGGACCCCCAAGACGTCGTCCTGACGAGCACCAACTCCGATTGA", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"}pooというのは、wgsimを利用して作られた適当なbamファイルです。
Rubyアソシエーション開発助成
このプロジェクトは2020年度のRubyアソシエーション開発助成に選ばれました。
この記事は以上です(未完)
- 投稿日:2020-12-18T14:47:41+09:00
次世代シーケンサーのデータをRubyで操作するライブラリを作りたい話 ruby-htslib
はじめに
こんにちは。この記事はバイオインフォマティクス Advent Calendar 2020 の記事です。
本当は、今日までにRuby-htslibの開発を進めて、公開したかったのですが、あまり進んでないので、もうあきらめて、背景的なことをタラタラと書いていこうと思います。
この記事は未完成ですが見切りをつけて公開します。ruby-htslibの開発が進んだら更新しようと思います。
Rubyに対するこだわり
R、Python, Perl, Julia, C++..世の中には星の数ほど素晴らしい言語があります。しかし私はRuby言語が好きです。私がたまたま最初に書けるようになったのはRuby言語だったからです。そこで、あたかもカルガモのヒナが最初に見たプログラミング言語を親だと思いこむように、Rubyが母国語になりました。(嘘です。Ruby言語の前にObjective-C言語とかに挑戦したことがありますが諦めました。Objective-C言語に比較すると、Rubyは単純で美しく見えました)
そんなRuby言語の特徴は、RやJulia等と比較すると、全てがオブジェクトで構成され、関数のかわりにメソッドを使うことです。この世界では「データ」と「手続き」がひとまとまりの「オブジェクト」として存在しています。これは一般的にデータ解析では不利な性質だと思います。一方で「何かを道具を作りたい」という人にとっては、力強いサポートになります。私も気がつくと、Rubyでツールを作ろうとしてしまいます。
Rubyでも次世代シーケンサーのデータを扱いたい
さて、Ruby言語はデータ解析にはあまり向いていないかもしれないと言いましたが、中には根強いファンもいて、BioRubyという生命情報用のライブラリもります。
- BioRuby: http://bioruby.org/
- リポジトリ: https://github.com/bioruby/bioruby
- ドキュメント: http://bioruby.org/rdoc/
けれども、今日の、何やらすごい研究のデータは、次世代シーケンサーの出力するデータを利用していることが多いです。そういった次世代シーケンサー関連のデータのファイル形式、つまり
SAM
,BAM
,VCF
,BCF
といったフォーマットを扱います。これを扱うのはBioRubyでは難しいらしい、ということが私にもわかってきました。誰か素敵なライブラリを作っていないのかな? と思って探しました。ひとつありました。Bioruby-samtoolsです。
- リポジトリ: https://github.com/helios/bioruby-samtools
- 論文: Bio-samtools: Ruby bindings for SAMtools, a library for accessing BAM files containing high-throughput sequence alignments
けれどもこのプロジェクトはSamtoolsをコマンドラインから呼び出すツールでして、細かくファイルを操作できるツールとはちょっと違う感じでした。(このプロジェクトは以前FFIを利用してSamtoolsのバインディングとして構成されていましたが、SamtoolsからHTSlibが分離された時にOpen3を使用するようになったそうです。Open3とは、プログラムを実行し、そのプロセスの標準入力・標準出力・標準エラー出力にパイプをつなぎます。つまりバインディングではありません。APIが安定したらFFIを利用してHTSlibのバインディングを作成する予定だったそうですが、開発者の方は余裕がなくてなかなか作成できなかったそうです)
一方で、Python、Rといった言語では次世代シーケンサーのファイルを利用できるライブラリが存在します。そして結果として解析用のtoolやパイプラインも多く開発されています。
普通であれば、目的に応じてツールを選択するということで、柔軟に言語を切り替えて利用するべきです。私も頭ではわかっているのですが、なかなかそのような合理的な方法をとることができません。モグラのように、なんとかしてRuby言語でもこれらのファイルを扱う方法はないだろうかと考えはじめました。
SAM/BAMというフォーマット
SAM
ファイルやBAM
ファイルといった形式は、リードのアラインメントを表したファイルです。次世代シーケンサーはFASTQというリードのファイルを生成します。このファイルを、bwa
やSTAR
といったアライナーでリファレンスゲノムにマッピングした結果のファイルがSAM/BAMファイルです。これは少し奇妙なファイル形式だと思います。私たちの身の回りには、SQLiteなどもっと一般的なデータベースがあります。タブ区切りテキストや、MS Excelといったファイル形式もあります。しかし生命情報の世界ではそれらのファイルではなく、SAM/BAMファイルが好んで利用されているようです。
@HD VN:1.6 SO:coordinate @SQ SN:ref LN:45 r001 99 ref 7 30 8M2I4M1D3M = 37 39 TTAGATAAAGGATACTG * r002 0 ref 9 30 3S6M1P1I4M * 0 0 AAAAGATAAGGATA * r003 0 ref 9 30 5S6M * 0 0 GCCTAAGCTAA * SA:Z:ref,29,-,6H5M,17,0; r004 0 ref 16 30 6M14N5M * 0 0 ATAGCTTCAGC * r003 2064 ref 29 17 6H5M * 0 0 TAGGC * SA:Z:ref,9,+,5S6M,30,1; r001 147 ref 37 30 9M = 7 -39 CAGCGGCAT * NM:i:1↑ 仕様書(hts-spec)に載っているSAMファイルの例。見慣れないヘッダー、フラグ、タグがついている。フラグは、リードがマッピングされているかいないかといった非常に重要な情報が含まれている。
- SAM/BAMに関連するリンク
フラグは2進数で表記されており、パット見ても何を意味しているのか人間にはわかりません。
samtools flag
コマンドを使うと、フラグの意味を表示してくれます。samtools flag 2064 # 0x810 2064 REVERSE,SUPPLEMENTARY↑ samtoolsでフラグの意味を表示したところ
Integer Binary Description 1 000000000001 ペアリードがある 2 000000000010 両方マップされている 4 000000000100 read1 はマップされていない 8 000000001000 read2(相方) はマップされていない 16 000000010000 read1 は逆相補鎖にマップされている 32 000000100000 read2(相方) は逆相補鎖にマップされてる 64 000001000000 read1 である 128 000010000000 read2 である 256 000100000000 複数個所にマップされている 512 001000000000 マッピングQVが低い 1024 010000000000 PCR or optical duplicate 2048 100000000000 supplementary alignment (e.g. aligner specific, could be a portion of a split read or a tied region) ↑ フラグの表
BAMファイルはSAMファイルの圧縮形式で、gzipと互換性のあるフォーマットだそうです。BAMファイルは圧縮されても検索しやすくなるように工夫されているようです。
CRAM
というよく似たファイル形式もあります。こちらはBAMよりも圧縮率が高いファイル形式のようです。これら特殊な形式のファイルを、Samtoolsというコマンドラインツールを使っていろいろ操作していくのです。実際のところ、この初心者に対する嫌がらせのようにも思えるファイル形式が使われている理由は、歴史的な経緯と、人類の技術的限界によるところが大きいのだと思います。RNA-SeqのBAMファイルは小さいものでも数GBあったりしますし、全ゲノムシーケンスなら数十GBにおよびます。これだけでも十分に巨大ですが、SQLiteやTSVにするとその数倍以上のサイズになるでしょう。そのような巨大なファイルをエクセルで検索することは当然難しくなります。解析するのも大変でしょうし、保存するスペースを確保するのも大変でしょう。サイズの問題、そして可能な限り高速に検索するために、特殊なフォーマットが開発されたということなのでしょう。(興味がある方は下のエントリーをお読みください)
- Forum: Why does bioinformatics use a special format like a BAM file instead of a database like SQLite?
- 煽り気味の質問にも親切に答えてくれるBiostarsのみなさん
しかしながら、コンピュータの性能は上がり続けています。いつまでも同じ状況が続くとは限りません。10年後、20年後には、現在の配列情報の処理は、スマートフォンで十分であるという時代が来るかも知れません。たとえばbiowasmとかいうプロジェクトがあります。これは、なんとWebAssemblyを使うことでブラウザ上で
samtools
bedtools
bowtie
seqkit
等を動かしてしまうというようなものだそうです。↑ ブラウザ上でゲノミクスのツールを動かす野心的なプロジェクトbiowasm
現状ではこのようなツールは役に立たないかも知れません。世間的には巨額な予算を獲得して、ワークフロー言語を使ってクラウド環境やスーパーコンピューターなど多数のPCを操作して、数の暴力により効率的に大量に検体を処理して新事実を発見しないとなかなか評価の対象にならないのかもしれません。しかしそんな勇ましいことを言っても、そんなことができるのは現実的には地球上でもごく少数の人だけでしょう。ですから、そのように物事をとらえてしまうと、どんどんと暗い気持ちになってしまうのではないでしょうかね。(少なくとも私は暗い気持ちになります)
ですから、そういった世知辛さとは遠いところにあるツールに触れたり眺めたりしている時間こそ、心あたたまる大切な時間だという気がいたします。
SAM/BAMのアイコンが見当たらない話
話がとびましたが、さらに話がとびます。
SAMファイルやBAMファイルのアイコンというものが見つからないことが気になっています。バイオインフォ用アイコンセットというものがないか結構探しました。コマンドライン上の操作に強い人はそんなの気にならないのかもしれませんが、個人的には気持ちよく作業をするためにはファイル・アイコンの存在はとても大事なことだと考えています。
もしもバイオインフォ用の素晴らしいアイコンセットを知っている方はコメント欄で教えてほしいなと思います。多分世界のどこかにはそういうものを作っている人がいるけれども、見つかってないだけだと思います。まあ自分でデザインして、身銭を切ってデザイナーの方に仕上げてもらい、それを著作権フリーで公開するという最終手段もありますが。
SAM/BAMの仕様書を読む
- SAM/BAM仕様書: https://github.com/samtools/hts-specs
は、Githubのリポジトリとして公開されています。日本語の情報があまりないので、詳しい内容を知りたい時は原典にあたる必要があります。しかし、PDFファイルであるため、Google翻訳がなかなか効きません。なので、PDFから文字起こしをして、Qiitaに記事として投稿しました。
↑のサイトをGoogle翻訳することによって、ある程度日本語でSAM/BAMファイルの仕様を把握することができます。
VCF形式とBCF形式
VCF/BCFは突然変異の形式です。こちらはまだ表面的にも理解できているとは言えないので、今後の勉強が必要です。まずはSAM/BAM形式のように仕様書を機械翻訳して全体の概要をつかもうと考えています。
Samtoolsの内部ではHTSlibが動いている
さて、このSamtoolsはC言語で作られていて、Samtoolsの内部ではhtslibというライブラリが、いわばエンジンのように動いていることがわかりました。HTSLibは、もともとSamtoolsの一部だったものをHTSLibとして切り分けたようです。そして、Python言語やJava言語など、いろいろな言語でhtslibに対するバインディングが作成されていることがわかりました。PythonやRのような言語でも、SAM/BAM/VCF/BCFを直接扱うようなツールは、htslibを間接的に利用していることを知りました。
↑ 各種言語におけるhtslibバインディング
面白いところはRustやNimといった、新しい静的型付き言語において熱心にバインディングが開発されていることです。これは、速度に対する需要が大きいのだろうなと思います。ゲノムのデータは膨大なので、Pythonのようなスクリプト言語よりも、RustやNim、Goといった言語を使って高速に処理することが求められているのでしょう。
上から5番目のJulia言語のバインディングは日本の方が開発されているようです。中をみてみますと、C言語のヘッダーファイルをパースしてバインディングをある程度自動生成しているようでした。
図:HTSLib.jl には独自のバインディング自動生成スクリプトが付属しているそこで私は考えました。Ruby言語も htslibのバインディングを作成すれば、ある程度は次世代シーケンサーのファイルが扱えるようになるのではないかと。
こういう時、PythonやR、Juliaなど流行の言語であれば、世界の素晴らしい方が作ったバインディングを探してどれを使えばいいか悩むのかも知れませんが、Rubyでは解決策は一つしかないのであれこれ悩む必要はありません。
自分で作るか、作らないかです。
HTSLibの論文が出たようです
また話がずれますが、偶然にも、HTSLibの論文が12月16日にアップロードされました。これを受けて、いろいろな言語でhtslibのバインディングの作成が活性化するかも知れません。
↑ Nim言語のバインディングhts-nimの作者が「便利そうダナー」とコメントしているツイッター
C言語のRubyのバインディングを作るということ
では、どうやってRubyのバインディングを作っていけばいいのだろうか、ということになります。方法は大きく分けて2通りあります。ネイティブ拡張を利用する方法と、FFIを利用する方法です。ネイティブ拡張を使う方法は、C言語をゴリゴリ書いていく必要があります。私はC言語を扱えませんのでこの方法を採用することはできません。もうひとつはFFIを利用する方法は簡単で、あまりC言語に詳しくなくても、Rubyバインディングを作ることができます。
私は1年前から、GR.rbというRubyでグラフを描出するツールを作っています。
↑ Rubyでもきれいなグラフや図が描出できるGR.rb
Julia言語では事実上のデファクトスタンダードとなっているGRというグラフ描出ライブラリのバインディングです。FFIはFiddleというRuby標準ライブラリを使っています。Rubyバインディングを作るのは、配線をつなぎ合わせてプラモデルを作るようなもので、そこまで高度な技術がなくてもできてしまいます。
先行している素晴らしいバインディングのコードを観察してパターンを学び、同じように作れば、ほとんどの場合うまく動作するものが作れます。このプロッティングツールも、Rubyでグラフを描出できるライブラリが足りないため、自分で作り始めたものです。
RubyコミュニティではAndrew Kaneさんという方が、2年ほど前から精力的にC言語の機械学習のツールのバインディングを作成されており、RubyでC言語のバインディングをFFIを利用して作ろうとする方は、Ankaneさんのリポジトリを観察すると、とても参考になると思います。
Rubyのhtslibバインディングを作る意味
次に、htslibのRubyバインディングを作る意義は何かという話になります。
開発する個人的なメリットとしては、ruby-htslibを作っているうちに、SAMやVCFのファイル仕様を読む必要が出てくるため、自然にフォーマットに対する知識がいろいろ身につくのではないかという気がします。また、これから先、次世代シーケンサーを超えて、さまざまな新しいモダリティが出現する時がかならず来ると思います。その場合も、いろいろなフォーマットが出現し、それを処理する基盤となるツールやライブラリが作成されて、各種言語からのバインディングが作られるという人間の営みは大きく変わりません。共通の課題に対していろいろな人々が頑張っている場を学ぶことには大きな意義がある気がします。
つぎに、Rubyよりももっとデータ解析に適した言語がいろいろ存在する中で、Ruby言語でバインディングを作る社会的な意味を考えます。これはやってみないとわかりません。RubyはActiveRecordに代表されるように、データをオブジェクトに変換して扱うという分野では使いやすさに定評があります。ですから、作り続けていくうちにHTSLibのRubyバインディングの良さのようなものが現れるとしたら、一つそういった方面があると思います。もちろんRuby言語を利用した解析ツールや、解析パイプラインのようなものが作れればそれに越したことはないですが、これはなかなか実現が難しいのではないかと考えています。いずれにせよ、プログラミング言語の世界でも多様性は大事であると考えます。
基本的な方針
さて、ここから先は、技術力がない人が、どうすればruby-htslibを作れるだろうか?という現実的な話になります。
最初のポイントはFFIのライブラリ選びです。Ruby言語では、Ruby-FFIと、Fiddleの2種類のFFIバインディング作成のためのライブラリがあります。どちらも歴史あるライブラリです。Ruby-FFIはより高機能なサードパーティのライブラリです。FiddleはRubyの標準ライブラリですが機能は簡易的で普及度はあまり高くありません。
- Ruby-FFI: https://github.com/ffi/ffi
- Fiddle: https://github.com/ruby/fiddle
今回Ruby-FFIを採用することにしました。Ruby-FFIは多くのプロジェクトで採用されており、Ruby配列とCポインタの相互変換、ネストされた構造体などを扱うのが得意です。Ruby-FFIのあまりよろしくない点としては、Ruby標準ライブラリではないため追加のライブラリのインストールが必要になることと、将来的にRubyの構成が大きく変わった時に変化についていけないリスクがあること、日本人開発者がいないため、日本語で質問しにくいという点があります。
↑まずはRuby FFIの全体像を得るために、Google翻訳を使って FFI wikiの全訳を作成しました。
さて、低レベルのバインディングでRuby-FFIを利用しても作るのが大変だというところは2点ほど思いつきます。
一つはRuby-FFIがC言語の bit field に対応していないところです。FFIを bit field に対応させましたというGemもあるのですが、公開日が古く、そのまま使うわけにはいかないと思っています。
もう一つ難しい点は、メモリ管理です。
Rubyではガーベッジコレクションが参照されなくなったオブジェクトを自動的に削除します。そのタイミングでFFIもメモリを開放するようです。しかし、何かの理由でうまく動作せず、開放するべきメモリが開放されなかったり、逆にGCが勝手にメモリを開放してしまうような場面が出現するかも知れません。そうなったとき、その問題を発見したり、デバッグしたりするのは私には大変難しいだろうなと考えています。
そういったものに当たったときには、人の助けを借りるしかないと思います。自動的にCのヘッダーファイルをパースしてバインディングを生成できないのか?
FFIのバインディングを作る場合、通常はC言語で書かれたヘッダーファイルの関数を、正規表現などを用いて置換を繰り返して、RubyのFFIモジュールに書き換えるのではないかと思います。けれども自分で一つ一つ関数をメソッドに書き換えるケースも悪くはありません。一つ一つのメソッドを手書きして実装するのは、時間と手間をかければ、誰でもやることができます。単調な作業に見えますが、実は大きなメリットがあります。それはライブラリのAPIを写経することで、ツールの全体像を深く理解できるようになることです。
けれども人間が手作業でバインディングを作成する場合、どうしても書き損じやタイポが生じます。HTSLibは現在でも開発が続いており、今後も関数は追加されたり、変更されたりします。HTSLibのバージョンが上がれば、Rubyバインディングも微修正を加えなければなりません。私は手作業は好きですし大事だと思っていますが、それでけでなく、何らかの自動化も考えていきたいところです。
一つの方針は、Julia言語のケースのように自分で変換器を作成することです。この場合、正規表現による置換を利用したり、Ruby言語で実装されたC言語のパーサを使って、バインディングを自動生成するスクリプトを作成すれば良いでしょう。もう一つは、RustのHTSlibバインディングのように、すでにある誰かが作った変換器を利用することも考えられます。
Ruby-FFIには自動的にバインディングを生成するプロジェクトがありましたが開発が事実上中断しています。最終更新日が古いのでそのまま使うことはできないでしょう。
また、次のようなプロジェクトもあるようです。こちらはうまく動作するようですので、利用できないかどうか検討しています。
nmコマンド
関連する方法として、共有ライブラリのバイナリファイルを直接観察するという考え方があります。
nm htslib.soとすると、共有ライブラリのバイナリファイルから直接関数の名前を見ることができます。
実装できる関数をなるべくカバーするという目的では、このようなバイナリを直接見る手段もどこかで役に立つかも知れません。Fat Gemの問題 共有ライブラリをパッケージに含めるべきかどうか?
共有ライブラリをパッケージ含めるメリットは、バージョンの調節が容易になることです。しかしGemのサイズが大きくなりますし、ユーザーによってはGemの中にバイナリファイルが含まれていることは好まず、自分で用意したhtslibを使いたいと考えるはずです。また、クロスプラットフォームにすることを考えた時、gemをプラットフォームごとに分割しない場合は、Windows, Mac, Linuxの3種類のバイナリを梱包しておく必要があります。(詳しくないのでもっとよい方法があるかも)
共有ライブラリを探す/指定する方法には、環境変数でパスを指定する方法のほか、Pkg-configを利用して、ファイルを見つける方法などがあります。htslibもpkg-configに対応しているっぽいです。しかしAnacondaなどでhtslibを入れた場合などの動作確認が必要と思われます。
高レベルのAPI
ffiを使ったバインディングは、2種類のAPIを作成します。
低レベルのAPIは、FFIモジュールを作成して、特異メソッドとして用意します。FFIモジュールの特異メソッドを呼び出すと、htslibの関数が呼び出されます。
高レベルのAPIでは、よりRubyらしいAPIを提供します。Rubyらしさとは何か、というのは難しい問題です。NGS関連ファイルを無理なくオブジェクト指向で扱えることが目標になると思います。まだしっかり理解できていないのでhts-pythonを移植して、それを改良することを考えています。
bamファイルを検索するような使い方では、ActiveRecordsのAPIが参考になるかもしれません。私はActiveRecordの使い方は詳しくないので、いろいろ勉強する必要があります。
高レベルのAPIを考えるためには他の言語のツールとかをよく使ってみてhtslib全体を理解する必要がありそうです。私の実力はまだまだで、ここからかなり勉強と経験が必要な気がするので、年単位で実現していけばなあと思っています。
現在の進捗状況
今の段階では、低レベルのAPIを主に手書きで作り終えたところです。
次のようなサンプルで、bamファイルを読み込むことができます。
require 'htslib' bam_path = File.expand_path('../assets/poo.sort.bam', __dir__) htf = HTS::FFI.hts_open(bam_path, 'r') idx = HTS::FFI.sam_index_load(htf, bam_path) hdr = HTS::FFI.sam_hdr_read(htf) b = HTS::FFI.bam_init1 nuc = { 1 => 'A', 2 => 'C', 4 => 'G', 8 => 'T', 15 => 'N' } cig = { 0 => :BAM_CMATCH, 1 => :BAM_CINS, 2 => :BAM_CDEL, 3 => :BAM_CREF_SKIP, 4 => :BAM_CSOFT_CLIP, 5 => :BAM_CHARD_CLIP, 6 => :BAM_CPAD, 7 => :BAM_CEQUAL, 8 => :BAM_CDIFF, 9 => :BAM_CBACK } 10.times do HTS::FFI.sam_read1(htf, hdr, b) p b[:core].members.zip(b[:core].values) p name: b[:data].read_string, flag: b[:core][:flag], pos: b[:core][:pos] + 1, mpos: b[:core][:mpos] + 1, mqual: b[:core][:qual], seq: HTS::FFI.bam_get_seq(b).read_bytes(b[:core][:l_qseq] / 2).unpack1('B*') .each_char.each_slice(4).map { |i| nuc[i.join.to_i(2)] }.join, cigar: HTS::FFI.bam_get_cigar(b).read_array_of_uint32(b[:core][:n_cigar]) .map { |i| s = format('%32d', i.to_s(2)); [s[0..27].to_i(2), cig[s[28..-1].to_i(2)]] }, quご覧のように、低レベルのAPIだけ使って無理やり表示しているのでごちゃついています。
まだまだですね…。先は長いです。これを実行すると次のように表示されます。[[:pos, 3289], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3289], [:isize, 0]] {:name=>"poo_3290_3833_2:0:0_2:0:0_119", :flag=>133, :pos=>3290, :mpos=>3290, :mqual=>0, :seq=>"GGGGCAGCTTGTTCGAAGCGTGACCCCCAAGACGTCGTCCTGACGAGCACAAACTCCCATTGAGAGTGGC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3292], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 3], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3292], [:isize, 0]] {:name=>"poo_3293_3822_1:0:0_0:0:0_76", :flag=>133, :pos=>3293, :mpos=>3293, :mqual=>0, :seq=>"TTCGATGCGGGACCCCCAAGACGTCGTCCTGACGAGCACAAACTCCCATTGAGAGTGGCACATGATTTCC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3293], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3293], [:isize, 0]] {:name=>"poo_3294_3861_2:0:0_2:0:0_2d7", :flag=>69, :pos=>3294, :mpos=>3294, :mqual=>0, :seq=>"TGGGGACCGTGTGACTATCAGAAGGGTGGGGTCAGCTTGTTCGATGCGGGACCCCCAAGACGTCGTCCTG", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3298], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3298], [:isize, 0]] {:name=>"poo_3299_3808_0:0:0_4:0:0_2e0", :flag=>69, :pos=>3299, :mpos=>3299, :mqual=>0, :seq=>"CCCAAGACGTCGACCTGAGGAGCACAAACTCCCAATGAGAGTGGCACATGATTTGCCCAACCATACCATT", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3303], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3303], [:isize, 0]] {:name=>"poo_3304_3813_0:0:0_2:0:0_2f5", :flag=>133, :pos=>3304, :mpos=>3304, :mqual=>0, :seq=>"GGACCCCCAAGACGTCGTCCTGACGCGCACAAACTCCCATTGAGAGTGGCACATTATTTCCCCAACCATA", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3311], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 3], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3311], [:isize, 0]] {:name=>"poo_3312_3825_0:0:0_1:0:0_6f", :flag=>133, :pos=>3312, :mpos=>3312, :mqual=>0, :seq=>"TTGTTCGATGCGGGACCCCCAATACGTCGTCCTGACGAGCACAAACTCCCATTGAGAGTGGCACATGATT", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3317], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 133], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3317], [:isize, 0]] {:name=>"poo_3318_3847_3:0:0_1:0:0_142", :flag=>133, :pos=>3318, :mpos=>3318, :mqual=>0, :seq=>"CTATCAGAAGGGTGGGGGCAGCTTGTTCGATGCGGGACCCCCAAGACGACGTCCTGACGAGCACAAACTC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3319], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3319], [:isize, 0]] {:name=>"poo_3320_3822_3:0:0_6:0:0_333", :flag=>69, :pos=>3320, :mpos=>3320, :mqual=>0, :seq=>"TTCGATGCGGGACCCCCAAGACGTCGTGCTGACGAGCACAACCTCGCAATGAGAGTGGCACAAGATTTGC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3321], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3321], [:isize, 0]] {:name=>"poo_3322_3847_1:0:0_0:0:0_2cb", :flag=>69, :pos=>3322, :mpos=>3322, :mqual=>0, :seq=>"CTATCAGAAGGGTGGGGGCAGCTTGTTCGATGCGGGACCCCCAAGACGTCGTCCTGACGAGCACAAACTC", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"} [[:pos, 3327], [:tid, 0], [:bin, 4681], [:qual, 0], [:l_extranul, 2], [:flag, 69], [:l_qname, 32], [:n_cigar, 0], [:l_qseq, 70], [:mtid, 0], [:mpos, 3327], [:isize, 0]] {:name=>"poo_3328_3840_1:0:0_2:0:0_211", :flag=>69, :pos=>3328, :mpos=>3328, :mqual=>0, :seq=>"AAGGGTGGGGGCAGCTTGTTCGATGCGGGACCCCCAAGACGTCGTCCTGACGAGCACCAACTCCGATTGA", :cigar=>[], :qual=>"2222222222222222222222222222222222222222222222222222222222222222222222"}pooというのは、wgsimを利用して作られた適当なbamファイルです。
Rubyアソシエーション開発助成
このプロジェクトは2020年度のRubyアソシエーション開発助成に選ばれました。
この記事は以上です(未完)
- 投稿日:2020-12-18T14:33:03+09:00
RailsとVueでアプリを作るための環境構築
この記事はRailsとVueでHello Vue!をすることを目的としています。
プロジェクトの作成
何はともあれrails newですよね。ちなみにこの時点で--webpack=vueオプションでvueを始めからインストールできますが、今回はそれ以外の方法を紹介します。
と言ってもrails webpacker:install:vueをあとで叩くだけです。% rails -v Rails 6.0.3.4 % rails new memo-memo -d mysql --skip-test % cd memo-memo実はmysqlのインストールで躓いてそちらの記事も書いたので参考にしてください。
今回は失敗していない体(てい)で進みます。
rails newでmysqlのインストールに失敗するデータベースの作成
% rails db:create Created database 'memo_memo_development' Created database 'memo_memo_test'Hello World!
% rails s Webpacker configuration file not found xxx/memo-memo/config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - xxx/memo-memo/config/webpacker.yml (RuntimeError)webpackerがインストールされていないと怒られたので
% rails webpacker:install % rails s => Booting Puma => Rails 6.0.3.4 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.6.3-p62), 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 stopWebpackerとは
世界に挨拶することができたので、Webpackerを用いてVueを使えるようにしていきます。
ここでWebpackerとは何かわからない方向けに説明すると、WebpackerとはRailsにWebpackを入れるためのライブラリーで、Webpackの設定をよしなにしてくれています。WebpackとはJSなどのファイルをひとつにまとめてくれるものになります。カッコよく言うと、モジュールバンドラーです。ファイルをひとつにまとめる理由はブラウザの読み込み速度を速くするためです。CPUで計算するのに比べて、ファイルを取りに行く方が圧倒的に時間がかかる処理で、ファイルを読み込む回数を減らすことがブラウザの読み込み速度に大きな効果があります。ちなみにコンパイルはWebpack本来の機能ではなく、あくまでwebpackerにloaderを入れることで実現しています。
また、実務ではWebpackerではエラーが起きた時に何が原因かわかりづらくなってしまうため、楽せずWebpackを使うらしいです。ただ、私と同じ初学者の方はWebpackerから入って問題ないと思います。
話が長くなりそうなので次に進みます。Vueのインストール
% rails webpacker:install:vueいくつかのファイルが追加されたと思いますが、重要なファイルはapp/javascript/packs/hello_vue.jsとapp/javascript/app.vueになります。これらのファイルでhello vue!ができるようになっています。
app/javascript/packs/hello_vue.jsimport Vue from 'vue' import App from '../app.vue' document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ render: h => h(App) }).$mount() document.body.appendChild(app.$el) console.log(app) })app/javascript/app.vue<template> <div id="app"> <p>{{ message }}</p> </div> </template> <script> export default { data: function () { return { message: "Hello Vue!" } } } </script> <style scoped> p { font-size: 2em; text-align: center; } </style>何故、app.vueの他にhello_vue.jsが必要なのかと言うと、vueファイルを直接読み込まずにjsファイル介して読み込むためです。html.erbでhello_vue.jsを読み込めば、hello_vue.jsはapp.vueを読み込んでいるのでapp.vueを表示できます。app.vueファイルのscriptタグ内のmessageという変数にHello Vue!が定義されていて、templateタグ内のpタグの中に変数messageが書かれていることで、Hello Vue!が出力されることは何となくわかると思います。詳しい説明は割愛させていただきます。
Hello Vue!の表示
これからHello Vue!を表示するための簡単なページを作成したいと思います。
流れとしてはルーティング→コントローラー→ビューになります。
ここでは'localhost:3000/home'にアクセスするとHomeコントローラーのindexアクションにルーティングされて、indexアクションからapp/view/home/index.html.erbを表示させたいと思います。そのindex.html.erbでhello_vue.jsファイルを読み込むことでHello Vue!を表示します。それではルーティングの設定を行います。
routes.rbget 'home', to: 'home#index'この状態でlocalhost:3000/homeにアクセスするとどうなるかわかりますか?
'uninitialized constant HomeController'と出ていると思います。Homeコントローラが定義されていないので当たり前ですよね。Homeコントローラーを作っていきます。% rails g controller home create app/controllers/home_controller.rb invoke erb create app/views/home invoke helper create app/helpers/home_helper.rb invoke assets invoke scss create app/assets/stylesheets/home.scss作成されたapp/controllers/home_controller.rbファイルを開いて、indexアクションを追加します。
class HomeController < ApplicationController def index end endこれでindexアクションの定義は終わりです。何も定義しなくてもいいのは、暗黙的にrenderが呼ばれて、アクションと名前で対応付けられたテンプレートが実行されるからですよね。
ちなみにこの状態でlocalhost:3000/homeにアクセスするとどうなるかわかりますか?
missing a templateですよね。
次にapp/view/home/index.html.erbを作成します。作成したらhello_vue.jsを読み込んでください。
ビューでJavaScript packをインクルードするにはjavascript_pack_tag ''を使います。今回hello_vue.js を読み込みたいので、pack名の箇所にhello_vueを記載しています。<%= javascript_pack_tag 'hello_vue' %>それでは、http://localhost:3000/home にアクセスして「Hello Vue!」が表示されているか確認しましょう!
お疲れ様です。実際に開発していくとなると、rails sの他にbin/webpack-dev-serverのコマンドも実行していた方がいいです。このコマンドはJSファイルのホットリロードを行ってくれるものになります。rails sとbin/webpack-dev-serverをひとつのファイルに記述して、1つのコマンドで2つのコマンドを実行することも可能です。詳しくは説明しませんが、foremanというgemを必要とします。'foreman rails s bin/webpack-dev-server'で調べると出てくると思います。
また、bin/webpack-dev-serverについてはこちらの記事が参考になるかもです。
- 投稿日:2020-12-18T13:50:03+09:00
Slackで毎時間リマインド通知(時報)させる
いままで
3年前に作った以下の雑スクリプトをHerokuにあげてSchedulerを利用して、SlackBotから時報させてた。(今動くのかは不明)
require "rubygems" require "slack" tz = Time.now.localtime("+09:00") Slack.configure do |config| config.token = 'XXXXXXXXXXXXXXXXXXXXXXXXXXX' end if !tz.saturday? && !tz.sunday? case tz.hour when 9 then message = '9時: おはようございます!今日の目標を明確にしましょう!' when 10 then message = '10時: 午前は捗りますね。その調子です。今日1日101%成長していきましょう!' when 11 then message = '11時: もうそろそろランチタイムです。あと少し頑張りましょう!' when 12 then message = '12時: ランチタイムです。コンビニばかりにならないように!' when 13 then message = '13時: 午後が始まりました。時間が早く流れるので気をつけて下さい!' when 14 then message = '14時: 集中力が切れていませんか?そんな時はチョコとコーヒーです!' when 15 then message = '15時: おやつなんて食べている場合ではありません。働いてください!' when 16 then message = '16時: 午後も3時間経過、今日の目標は達成できていますか?' when 17 then message = '17時: ここから切り替えて、さらにもうワンステップ先にいきましょう!' when 18 then message = '18時: お疲れ様です。業務時間は終了です。今日の振り返りをしましょう。' end Slack.chat_postMessage( text: message, channel: '#general', username: '時報bot', icon_url: 'https://i.gyazo.com/763d65291a1bef993f7dac57ce7ec643.png' ) endこれから
文言やチャンネルなど変更するのに、いちいちデプロイするのがめんどくさく、LambdaやS3(静的テキストとして置いておく)など検討したが、
そこまでする?と思ったので、、、Slackのリマインダーにした。every hour
はできないとのことなので、以下のようにした。
対象のチャンネルで、1行ずつコピペして貼るだけ。/remind #general @izumiru 9:00です at 9:00 every day /remind #general @izumiru 10:00です at 10:00 every day /remind #general @izumiru 11:00です at 11:00 every day /remind #general @izumiru 12:00です at 12:00 every day /remind #general @izumiru 13:00です at 13:00 every day /remind #general @izumiru 14:00です at 14:00 every day /remind #general @izumiru 15:00です at 15:00 every day /remind #general @izumiru 16:00です at 16:00 every day /remind #general @izumiru 17:00です at 17:00 every day /remind #general @izumiru 18:00です at 18:00 every day /remind #general @izumiru 19:00です at 19:00 every day /remind #general @izumiru 20:00です at 20:00 every day平日だけなら
day
をweekday
に変更する。/remind #general @izumiru 9:00です at 9:00 every weekday逆に一般的に時報はどうやっているのか知りたい。
mac標準搭載の音声時報はいや。。。。単純にプッシュ通知が来て欲しい。
- 投稿日:2020-12-18T13:22:21+09:00
deviseについて
最初に
カレンダー企画2020の18日目
プログラミングの勉強を始めて3ヵ月程経ったので学んだことのメモをアウトプットとして記事に残します。
これからプログラミングの世界に入る人の手助けになれたら嬉しい限りです。
間違っていたり、言葉が違っていたり、誤解されるような言葉があったら教えてください^^
言葉を長々と読みづらかったら申し訳ありません。少しずつなれてがんばります。gemについて
Rubyにはgemという便利なものがあります。
自分でいちから機能を作る事もできるのですが、複雑や大変だったりするのでそういった手間を省いて簡単に機能をアプリケーションに追加できるものです。開発スピードを上げることができます。
(注:頼りすぎは良くないです!って言われました^^)そのgemの1つに「devise」というのがありそれを今回は取り上げでいきます。
deviseとは?
deviseとは一体何をする機能があるのか?
よくネットのサービスを使用する際に、ユーザー登録を行いませんか?
新規登録とか、ログインとかそれができるgemです!
こういった機能も1から作成できるみたいですがこれを使えば簡単に機能として追加できます!
導入の仕方
まずはインストールから
Gemfile. . . . . . gem "devise"アプリケーションを作成した時にGemfileというのが作成されていると思います。
それを開いて一番下に上記のを追記してください。ターミナル$ bundle installbundleもgemの一種ですがdeviseと違い機能を追加ではなく、gemを管理するものです。
ターミナル$ rails g devise:installdeviseの初期設定をこれで行います。
導入の準備はこれで完了
ログイン画面を表示させてみる
ユーザーテーブルを作成する。
テーブル作成も本来なら1から行いますがdeviseを使うと自動で生成してくれます。ターミナル$ rails g devise モデル名 #通常はrails g model モデル名ですがdeviseバージョンは左記そうするとマイグレーションファイルが生成されています。
その中はこんな感じになっているかと、、、def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable # t.integer :sign_in_count, default: 0, null: false # t.datetime :current_sign_in_at # t.datetime :last_sign_in_at # t.string :current_sign_in_ip # t.string :last_sign_in_ip ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true # add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end endこれはdefaultです。
とくに問題なければこのままですし、
テーブル内にユーザーの名前や住所とか必要なカラム名はアプリケーションに合わせて追記をしてください。追記が終わったら、
ターミナル$ rails db:migrate他にも
config/routes.rb
やapp/model/モデル名.rb
にもdeviseの設定が記載されているはずです。実はこれでもうログインページは使えます!^^
補足
ログインページのレイアウトを変更したい時は
ターミナル$ rails g devise:views #ファイル名を設定したい時はviewsの後に記述ログイン情報を追加した時
初期設定ではメールアドレスとパスワードのみになっている。
実際のアプリケーションでは名前などもログインの条件に入れたい!時に使いますapp/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) devise_parameter_sanitizer.permit(:sign_in, keys: [:name]) devise_parameter_sanitizer.permit(:account_update, keys: [:name, :email]) end end
before_action :configure_permitted_parameters, if: :devise_controller?
devise機能を使う前
configure_permitted_parameters
の処理を行うよ!と定義
下記に定義された内容が実行される。
devise_parameter_sanitizer.permit
データ操作を許可するメソッドです。
1行目:サインアップの際にnameのデータ操作が許可される。
2行目:サインインの際にnameのデータ操作が許可される。
3行目:アカウント情報更新の際にname,emailのデータ操作が許可される。
protected
これはストロングパラメーターと同様の機能です。少し違うのは、他のコントローラーから呼び出されても参照できるという点です。
private
こっちは記述されたコントローラー内だけです。最後に
deviseはとても便利なものです。うまく活用していけるといいですね。
deviseのコントローラーも追記した記憶あるので自分なりのカスタマイズはもっとできると思います。
ファイルの追加方法はviewsをcontrollersに変えただけだった気がします。(違ったらごめんなさい)
- 投稿日:2020-12-18T13:11:19+09:00
第六回
variable
今回は変数とメソッドについて取り扱っていきます
Theme
受け取った引数ARGV[0]をnameという変数に代入してそれを打ち出すプログラムを作る.
> ruby name_variable.rb Ruby Ruby解説
rubyにおける変数はC言語のように型宣言する必要がないので,以下のように記入すればよい.
name='Ruby'変数の種類
Rubyには,以下のように様々な変数が存在します.
・ローカル変数・定数・グローバル変数・ブロック変数・インスタンス変数・クラス変数・クラスインスタンス変数
ローカル変数
解説に載っていた変数です.一番よくつかわれます.
宣言
変数名には英数字と_(アンダーバー)を使うことができます.先頭には数字を使うことはできず,宣言と初期化を同時に行う必要があります.
name="Ruby" #ok name_1="Ruby" #ok 2_name="Ruby" #NG ->SyntaxError name #NG ->NameErrorスコープ
メソッド,クラスなど定義された場所の範囲内でのみ使用可能.クラスやメソッドを超えての参照はできず,定義された場所以外から参照をするとエラーになる.
定数
宣言
ルールはローカル変数と同じだが,定数は全て大文字で宣言する.最初の文字だけ大文字にする「キャメルケース」はクラス名などに用いられる.
NAME="ruby" Name="ruby" #クラス名と紛らわしいので定数としては使われないスコープ
定義されたクラスやモジュール内やクラスを継承しているクラスなどで参照できる.
RUBY=2.78 puts RUBY2.78クラス内で定義可能なので同じ定数名でもクラスごとに違う値をもつことができる.
グローバル変数
宣言
変数名の一番初めに"$"をつけないといけない.
$name="ruby"スコープ
ローカル変数の逆で,メソッドやクラスを超えて参照できる変数.定義した場所にとらわれず,プログラムのどこからでも参照や変更ができる.
$name="ruby" def greet p $name end greet"ruby"ブロック変数
宣言
ブロック内「{}(ブレース)もしくはdo ~ end 内」で「|」で囲むことで宣言できる.
#複数行でまとめる場合 3.times do |i,x| end #1行でまとめる場合 [1,2,3].inject {|sum, n| sum+n}スコープ
ブロック実行の時にブロック内で用いられる一時的な変数.繰り返しなどの処理を行う際,「ブロック変数」に値を代入しながら処理を行う.ブロック内からのみ参照可能.
num=99 3.times do |i, x=num| num=i*10 p x end10インスタンス変数
宣言
変数の始めに「@」をつける.
@name="ruby"スコープ
インスタンスメソッド内でのみ使用できる変数.インスタンスごとに異なる値を持つことができ,メソッドを超えて参照することができる.
class User def initialize(name) @name=name end def put_name p @name end end user1=User.new("ruby") user2=User.new("python") user1.put_name user2.put_name"ruby" "python"クラス変数
宣言
変数のはじめに「@@」をつける.
@@name="ruby"スコープ
定義されたクラス内で共有する値を扱うことができる変数.他のインスタンスで使用しても同じクラス内であれば共通の値として扱われるので,クラス内では一つの値しか扱えない.クラスを継承したクラスからも参照できる.
class Ruby @@number=1 def foo puts @@number end end num=Ruby.new num.foo1class Jewelry < Ruby def hoge puts @@number end end num2=Jewelry.new num2.hoge1このように,Rubyクラスを継承したJewelryクラスでもクラス変数にアクセスできていることが分かる.
クラスインスタンス変数
宣言
インスタンス変数と同じく「@」で始まるが,定義される場所によりインスタンス変数と区別される.
@class_instance_var="ruby"スコープ
クラス変数と同様,クラス内で定義し,定義されたクラス内でしか参照することができない.
インスタンス変数と同じく@で始まるが,クラスで定義されている場合はクラスインスタンス変数,インスタンス内で定義されている場合はインスタンス変数と定義されている場所で区別される.クラスを継承したクラスからは参照できないのがクラス変数との違い.
class MyClass @class_instance_var="ruby" def sef.class_instance_var puts @class_instance_var end def class_instance_var puts @class_instance_var end end instance=MyClass.new instance.class_instance_varインスタンスメソッド内では,インスタンス変数として認識されるので,クラスインスタンス変数にはアクセスできないことが分かる.
class MyClass @class_instance_var="ruby" def self.class_instance_var puts @class_instance_var end def class_instance_var puts @class_instance_var end end MyClass.class_instance_varrubyクラスメソッドの場合はクラスインスタンス変数にアクセスできる.
class SubClass < MyClass def self.class_instance_var_again puts @class_instance_var end end SubClass.class_instance_var_again継承したクラスからはクラスインスタンス変数にアクセスできない.
参考文献
https://www.sejuku.net/blog/12879
- source ~/grad_members_20f/members/NobuakiMori/L06_method.org
- 投稿日:2020-12-18T12:58:50+09:00
Rspec: subjectにメソッドを設定した場合に、実行後の状態をテストしたい
前提
context "ユーザを生成" do subject { User.create(name: "John") } it "ユーザが1つ生成される" do is_expected.to change(User.count).by(1) end endsubjectとして、ユーザ生成のためのメソッドを定義しました。
この状態で、生成されたユーザ名が"John"であることを、同じsubjectでテストしたいことがありました。うまくいかない場合
it "ユーザ名がJohnである" do subject expect(User.last.name).to eq("John") endこうしてもUser.lastがnilですよ、というエラーが出て、どうやらsubjectの中身が実行されていない様子。
原因究明
puts subject.classとしてクラスを調べると、
Proc
と返ってくる。
rubyではProcクラスという、実行コード自体を変数に格納するクラスが存在しています。
したがって
subject
として書いても
"文字列"
とか書いているのと同じようなもので、何も起こりません。
したがってProcクラスのオブジェクトとして実行してやる必要があります。解決方法
実行の方法は様々ですが、一例でいうと
subject.callがあります
したがって
it "ユーザ名がJohnである" do subject.call expect(User.last.name).to eq("John") endこれでパスしました。
別のコンテキストにしろよ、とか、subjectの設定がいまいち、とかもあるかもしれませんが、同じ状況に直面したらぜひご一考をば。
全体のコード
context "ユーザを生成" do subject { User.create(name: "John") } it "ユーザが1つ生成される" do is_expected.to change(User.count).by(1) end it "ユーザ名がJohnである" do subject.call expect(User.last.name).to eq("John") end end以上です。
- 投稿日:2020-12-18T12:04:17+09:00
countメソッドの落とし穴(クエリの大量発行に注意!)
はじめに
私は現在
rails
を使って転職活動用のポートフォリオを作っています。その中でbullet
というN+1問題
を検知してくれるgemを使っていたのですが、それでは検知できずに無駄にクエリを発行していたことに気づいたので記事を残しておこうと思います。原因としては、「投稿一覧ページにおいて、いいねの合計数を取得するさいにcountメソッド
を使っていたこと」です。N+1問題とその具体例
知っている人は読み飛ばしてください。
N+1問題
とは、アソシエーションが組まれたテーブルのカラムを参照する際に必要以上にクエリを発行してしまう問題です。サーバー側に余計な負荷をかけ、ページの読み込みを遅くする原因になります。簡単な例としてUser
モデルとPost
モデルで次のようなテーブル設計が組まれている場合に、投稿一覧ページですべての記事と投稿者の名前を出力するといった状況を考えます。users テーブル
Column Type Options name string null: false Association
- has_many :posts
posts テーブル
Column Type Options user references null: false, foreign_key: true text string null: false Association
- belongs_to :user
N+1問題が発生する実装方法
app/controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.all end endapp/views/index.html.erb<% @posts.each do |post|%> <%= post.user.name %> <%= post.text %> <% end%>この方法だと例えば3件の投稿があった場合、
@posts
と呼び出したときにpostsテーブル
の全レコードを取得(クエリ1回発行)し、post.user.name
で投稿の数だけ(クエリ3回発行usersテーブル
にアクセスしなければなりません。これがN+1問題
と言われる所以です。N+1問題の発生しているクエリPost Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`created_at` BETWEEN '2019-12-17 00:00:00' AND '2020-12-17 23:59:59.999999' ORDER BY `posts`.`created_at` DESC LIMIT 6 OFFSET 0 ↳ app/views/posts/_index_posts.html.erb:2 User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/views/posts/_index_posts.html.erb:7 CACHE User Load (0.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/views/posts/_index_posts.html.erb:7 CACHE User Load (0.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 ↳ app/views/posts/_index_posts.html.erb:7N+1問題を解決する実装方法
コントローラーで
preloadメソッド
を使います。また、includesメソッド
を使っても同じ動作をします。app/controllers/posts_controller.rbclass PostsController < ApplicationController def index @posts = Post.all.preload(:user) end endこれによって
@posts
を宣言した際にアソシエーション先のuser
をまとめて取得できます。これによりクエリを2つに分けることです。1つめのクエリは関連データを取得するクエリ、2つ目のクエリは最終的な結果を取得するクエリという具合です。仮にuser
の数が増えたとしてもSELECTusers
.* FROMusers
WHEREusers
.id
IN (1, 2, 3,,,,)と一回で取得できます。N+1問題解消後のクエリPost Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`created_at` BETWEEN '2019-12-18 00:00:00' AND '2020-12-18 23:59:59.999999' ORDER BY `posts`.`created_at` DESC LIMIT 6 OFFSET 0 ↳ app/views/posts/_index_posts.html.erb:2 User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1countメソッドでいいねの数を取得すると、、
本題に入っていきます。僕はpostに対するいいねの数を取得する際に
@posts each do |post|〜end
のブロック内でpost.likes.count
のようにしていました。すると以下のようにeach内で都度、合計値を取得する余分なクエリが発生します。また、BulletはN+1であると検出してくれません。countメソッドによりN+1問題の発生しているクエリ↳ app/views/posts/_index_posts.html.erb:2 (0.2ms) SELECT COUNT(*) FROM `likes` WHERE `likes`.`post_id` = 7 ↳ app/views/posts/_index_posts.html.erb:25 (0.2ms) SELECT COUNT(*) FROM `likes` WHERE `likes`.`post_id` = 6 ↳ app/views/posts/_index_posts.html.erb:25 (0.2ms) SELECT COUNT(*) FROM `likes` WHERE `likes`.`post_id` = 5 ↳ app/views/posts/_index_posts.html.erb:25 (0.2ms) SELECT COUNT(*) FROM `likes` WHERE `likes`.`post_id` = 4 ↳ app/views/posts/_index_posts.html.erb:25 (0.2ms) SELECT COUNT(*) FROM `likes` WHERE `likes`.`post_id` = 3 ↳ app/views/posts/_index_posts.html.erb:25 (0.2ms) SELECT COUNT(*) FROM `likes` WHERE `likes`.`post_id` = 2これは
countメソッド
がEnumerableモジュール
に定義されたメソッドであるからです。(このモジュールのメソッドは全てeach
を用いて定義されています。Enumerableモジュールの詳細はこちら)そこで、この問題を解決する二通りの方法をみていきましょう。解決策①sizeメソッドを使う
先ほどの
countメソッド
をsizeメソッド
に変えれば解決します。しかしながら、ただ変えればいいというわけだはありません。これはsizeメソッドの定義を見ればわかります。sizeメソッドdef size loaded? ? @records.length : count(:all) endこれはすでに読み込まれた配列があれば、そのlength(要素数)を取得しますが、そうでない場合COUNTクエリが実行されることを意味します。これを解決するには先ほどのpreloadメソッドを使えばいいということです。これによりクエリは以下のようになります。
sizeメソッドによりN+1問題を解消したクエリLike Load (0.3ms) SELECT `likes`.* FROM `likes` WHERE `likes`.`post_id` IN (2, 3, 4, 5, 6, 7)解決策②カラム(likes_count)を追加する
railsでは関連づけられたモデルの数をカラムに持たせるオプションがサポートされています。それが
:counter_cache
オプションです。次のように使います。app/controllers/posts_controller.rbclass Like < ApplicationRecord belongs_to :post, counter_cache: :likes_count endこうすればPostsテーブルに
likes_count
というカラムをinteger型
で加えるだけで「いいねの数」を数をカラムとして持つことができます。したがって、post.likes_count
のように値を取得して表示するだけならクエリは発行されません。(いいねの数が変わった時は発行されます)まとめ
countメソッドを使って生じたN+1問題を解決する方法を2通り紹介しました。最後に説明したカラムを追加する方法だとpreloadしない分ページの読み込みは早くなるけど、「ユーザーがたくさんいて一斉にいいねボタンを押す」みたいな状況だと逆に大量に負荷がかかってしまうのかあなというのが個人の見解です。また、
:counter_cache
オプションはデッドロックも発生しやすいみたいです。これを解決する方法としてcounter_culture
というgemを使う方法があるみたいなのでこれも使ってみてそのうち記事にしてみたいと思います。
- 投稿日:2020-12-18T11:53:13+09:00
【Ruby】 クラスとインスタンスの概念を用いたコードの作成 ※復習
現在、論路的思考を鍛えるために復習をしています。
今回は模範解答とは別の記述方法でできたので残します。問題
ruby.rbclass Article def initialize(author, title, content) @author = author @title = title @content = content end end上記のコードに追加を行い、以下の出力結果を得られるようにする。
ただし、クラスとインスタンスを使用する。ruby.rb著者: 山田 タイトル: 正しい痩せ方 本文: 1食を脂質15g、糖質35gに抑える模範解答
ruby.rbclass Article def initialize(author, title, content) @author = author @title = title @content = content end def author @author end def title @title end def content @content end end article = Article.new("山田", "正しい痩せ方", "1食を脂質15g、糖質35gに抑える") puts "著者: #{article.author}" puts "タイトル: #{article.title}" puts "本文: #{article.content}"インスタンス生成時にインスタンス変数に代入する値を渡して、
インスタンス変数を返すためにメソッドを定義して、
最後に呼び出し、という流れですね。私も流れは同じですが、メソッドの中に出力したい文字も入れてみました。
ruby.rbclass Article def initialize(author, title, content) @author = author @title = title @content = content end def author puts "著者:#{@author}" end def title puts "タイトル:#{@title}" end def content puts "本文:#{@content}" end end article = Article.new("山田", "正しい痩せ方", "1食を脂質15g、糖質35gに抑える") article.author article.title article.contentこれでもできるのか?という仮説が合っていて嬉しいのですが、
どちらの方が好ましいのか分かりません。
教えていただける方がいらっしゃいましたら幸いです。
- 投稿日:2020-12-18T10:56:13+09:00
PaperclipからActiveStorageに移行した話
こんにちは。@mshibuyaです。
現在副業として株式会社ZENKIGENさんのお手伝いをしておりまして、Web面接サービスharutakaのRailsまわりの改善を担当しています。今回はそちらで行ったPaperclipからActiveStorageへの移行におけるあれこれの話をしたいと思います。なおこの記事はRails Advent Calendar 2020の18日目のエントリーです。
動機
2018年4月リリースのRails 5.2でActiveStorageが登場し、ほどなくPaperclipのdeprecationが発表され早いもので2年以上が経ちました。Paperclipはこれ以上メンテされないわけなので、別の手段を検討していく必要があります。
harutakaにおいてもPaperclipを長く利用してきており、新たな手段への移行を模索し既に部分的にActiveStorageを利用する構成となっていました。とはいえPaperclipを利用する部分がそのまま残っておりActiveStorageと併存している状態もメンテナンス上好ましくないので、このたび全体を新方式へと刷新することとしました。
検討した移行先
ファイルアップロード機能を提供するライブラリとしていくつかの選択肢があるので、それぞれの特徴を整理し移行先候補を選定しました。
ActiveStorage
前述の通り、Railsの標準機能の一部として実装されたファイルアップロード機能です。
Pros
- Railsの一部であり、今後Rails界隈でデファクトスタンダードとなっていくことが期待される
- 同様にアクティブなメンテナンスが継続する期待がある
- harutakaで既に部分的に使われている実績がある
- Paperclipがofficialに移行先として指定しており、移行手順もある
Cons
- PaperclipやCarrierWaveと比較して機能が劣る
- Opinionatedな作りであり、思想にマッチしない使い方をすると苦労しそう
kt-paperclip
Kreeti社によりメンテされているPaperclipのforkです。
Pros
- 実績がある枯れたライブラリであるPaperclipを踏襲している
- harutakaでも主要部分に使われているため移行の手間が少ない
- それなりに多機能
Cons
- 本家によるメンテナンスではなくfork版であり、今後の先行きが不透明
CarrierWave
Paperclipに次いでメジャーなファイルアップロードライブラリですね。
Pros
- 多機能
- @mshibuyaがメンテナなのでなにかあっても安心?
Cons
- 若干使い方が複雑
- harutakaでの利用実績がなく、まったく新規での導入になる
以上を総合的に踏まえ、既存のPaperclipによる実装をActiveStorage化することでActiveStorageへの一本化を行うこととしました。
移行にあたっての方針
使い勝手をなるべく既存のものに近づけたい
フルタイムで開発に関わっているわけでない立場上、他の開発者の方々に過度に負担かけたくないという意図がありました。
何か問題があったときに切り戻し可能にしたい
画像や動画を保存・閲覧できる機能はharutakaの中でも重要度の高い部分であるので、今回の移行において不具合等が本番リリース後にあったようなときはすぐにPaperclip実装に切り戻して普通に利用を続けられることを目指しました。
S3に既に保存されているデータの移行はせずに済ませたい
保存されたデータの移行をするのは時間がかかり、その間のシステム利用を止めるか移行中のデータ更新を反映できる仕組みを用意する必要があるので考えることが増えるため、少なくとも移行初期のタイミングでは行いたくないと考えました。
そのため、不足する機能についてはActiveStorageにパッチを当てることでなんとかすることを目指すわけですが…
ActiveStorageに足りなかった機能
まぁここでActiveStorageのシンプルかつopinionatedな作りにより色々と足りない機能が出てきます。どんな機能が足りなかったか、それをどうしたかをご紹介していくことにします。
なおここで例示しているコードはActiveStorage 5.2を想定しています。他のバージョンではそのまま動かないかもしれないのでよしなに読み替えていただければと思います。
CloudFrontの署名付きURLを利用してのファイル配信
まず、ActiveStorageはS3をバックエンドにしてのデータ保存および配信にはもちろん対応しているのですが、意外にもCloudFrontを利用した配信については標準ではサポートしていません。
とはいえこれの解決策は比較的簡単です。ActiveStorageにはserviceとしてローカルディスク・S3といった様々なストレージバックエンドを差し替えられるような作りとなっているので、
require 'active_storage/service/s3_service' module ActiveStorage class Service::CloudFrontService < ActiveStorage::Service::S3Service def url(key, expires_in:, filename:, disposition:, content_type:) instrument :url, key: key do |payload| generated_url = Aws::CF::Signer.sign_url "https://#{CLOUD_FRONT_HOST}/#{key}" payload[:url] = generated_url generated_url end end end endのようにS3Serviceを継承する形でCloudFrontServiceを作り、storage.ymlで
production: service: CloudFront access_key_id: xxx secret_access_key: xxx ...とすると「S3にファイルを保存し、CloudFrontの署名つきURLで配信」という状態が作れます。
(※ここではcloudfront-signer gemを使っていて、その設定は別途必要です)URLを受け取りデータを保存する機能
Paperclipは、ファイルそのものではなくURLを受け取るとそのURLからデータをダウンロードし保存するという機能があります。これはPaperclipのIOAdapterのひとつ、UriAdapterとして実装されているのですが、同様の仕組みはActiveStorageにはないためパッチとして実装する必要があります。
イメージこんな感じですね。ActiveStorage::Attachedをモンキーパッチします。
ActiveStorage::Attached.prepend Module.new { def create_blob_from(attachable) case attachable when String uri = URI.parse(attachable) rescue nil if uri.is_a?(URI::HTTP) file = DownloadedFile.new uri ActiveStorage::Blob.create_after_upload! \ io: file.io, filename: file.filename, content_type: file.content_type elsif attachable.present? super end else super end end } class DownloadedFile attr_reader :io def initialize(uri) @uri = uri @io = uri.open end def content_type @io.meta["content-type"].presence end def filename CGI.unescape(@uri.path.split("/").last || '') end endS3への保存先pathのカスタマイズ
PaperclipはURL Interpolationによりファイル保存先のpathを非常に柔軟性高く指定することを可能としています。一方、ActiveStorageはそういったカスタマイズの余地はなく、ファイルの保存先pathは常に
generate_unique_secure_token
により生成されたランダム生成された文字列となります。
ActiveStorageはかなり強い意志をもってこの対応を入れないことを選択しているようで、過去に寄せられているPRも却下しており将来的にも入る見込みはなさそうです…。なのでパッチしてなんとかします。モデル側でこのようにkeyを生成するprocを渡せるようにした上で、
has_one_attached :image, key: -> (filename) { "files/image/#{record.class.generate_unique_secure_token}/#{filename}" }このprocをActiveStorage::Blobまで引き回し
ActiveSupport.on_load(:active_storage_blob) do prepend Module.new { def key self[:key] ||= if attachment key_proc = options[:key] # 引き回してきたやつ (key_proc && attachment.instance_exec(filename, &key_proc)) || super else super end end } endと値がなければprocをinstance_execするようにして望み通りのkeyを生成します。
名前つきのstyle
Paperclipはサムネイル画像の生成についてstyleという概念を持っており、生成する画像サイズに名前をつけることができます。
has_attached_file :photo, styles: {thumb: "100x100#"}ActiveStorageももちろんサムネイル生成に対応しているのですが、こちらは画像保存時ではなく利用時に動的にサイズを渡し生成する方式です。
<%= image_tag user.avatar.variant(resize: "100x100").service_url %>でも、名前がついている方が用途がわかりやすいですし変に似たようなサイズの画像が乱立してしまうのを防げるので、こうできるようにしたいですよね?
<%= image_tag user.avatar.variant(:thumb).service_url %>そこでパッチします。モデル側から
has_one_attached :image, variants: {thumb: "100x100#"}こんな風に指定できるようにした上でまたこのoptionsをActiveStorage::Blobまで引き回して
ActiveSupport.on_load(:active_storage_blob) do prepend Module.new { def variants options[:variants] || {} # 引き回したやつ end def variant(style_or_transformations) if style_or_transformations.try(:to_sym) == :original self elsif variable? && variants[style_or_transformations] Variant.new(self, variants[style_or_transformations]) else super end end } endとすることで実現できます。
Paperclipのカラムに値を保存する
ActiveStorage実装をリリースしてしばらく使った後になにか問題が発覚して切り戻しを行う場面を想定します。ストレージバックエンドであるS3はPaperclip/ActiveStorageで共通して使うので問題ないとして、ActiveStorage移行後なので新規にアップロードされたファイルについてはActiveStorage側のテーブル(モデルでいうとActiveStorage::AttachmentおよびActiveStorage::Blob)には値が入っているものの、Paperclip側で使われていた各モデルのカラム(
*_file_name
,*_file_size
…など)には値が入らない状態になります。これでは切り戻しの際にはActiveStorage側からPaperclip側へ逆データ移行する作業が必要になってしまいます。それを防ぐため、ActiveStorage側にファイルをアップロードしたらPaperclip側で使われていたカラムにも値を書き込む処理を入れてみます。
モデルで
has_one_attached :image after_save { image.replicate_for_paperclip! }としておいて、ActiveStorage::Attached::Oneをパッチし
ActiveStorage::Attached::One.prepend Module.new { def replicate_for_paperclip! return unless attached? attributes = {} attributes["#{name}_file_name"] = filename.to_s if record.attributes.has_key?("#{name}_file_name") attributes["#{name}_content_type"] = content_type if record.attributes.has_key?("#{name}_content_type") attributes["#{name}_file_size"] = byte_size if record.attributes.has_key?("#{name}_file_size") attributes["#{name}_updated_at"] = blob.created_at if record.attributes.has_key?("#{name}_updated_at") record.assign_attributes(attributes) record.save! if record.changed? end }これでActiveStorageアップロード時にPaperclip側カラムにも値を埋めておけるようになります。
まとめ
PaperclipからActiveStorageへの移行を行ったこと、そこでActiveStorageに不足している機能をどのように補ったかをご紹介しました。上記方針により、アプリケーションの土台に関わる大きな変更ながらもなるべく低リスクで実施可能なよう作り上げることができたのではないかと考えています。
(とはいえ本番リリースはまだこれからなのですが。何も問題起こらないといいな…)Paperclipを使い続けてきており、今後どうするか決まっていないRailsアプリケーションをお持ちの方も少なからずおられると思うので、この記事が参考になれば幸いです。
皆様のファイルアップロードライフがよいものでありますように!
- 投稿日:2020-12-18T10:55:26+09:00
rails newでmysqlのインストールが失敗する
初めてのQiita投稿になります!当初はRailsとVueのタスク管理アプリを作りながら記事を書くつもりでしたが、rails newの時点でmyqlのインストールが上手くいかずに躓いたので、こちらの記事を書くことにしました。
ただ、解決方法は最後に書いており、それまでは解決方法にたどり着くまでのストーリーになっているので飛ばしてもらった方がいいです。筆者自身の備忘録も読みたい方は上から読んで頂ければと思います。
環境は見れば分かる人もいるかもしれませんが、Macです。(zshでbrewを使っていればMacですよね?)
最近知ったのですが、zshはジーシェルって読むらしいです。プロジェクト開始のはずが
何はともあれ、rails newですね。
% rails -v Rails 6.0.3.4 % rails new memo-memo -d mysql --skip-test早速エラーが発生しましたw
An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.言われた通りにやります。
% gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'Pemission errorが発生したのでここも思考停止でsudoをつけます。
% sudo gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'今度は良くわからないですね。
Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load ----- ----- Setting libpath to /usr/local/Cellar/mysql/8.0.22_1/libそもそもMacに正しくmysqlが入っていることを確認してみます。
するとBrewさんは私に不満があるらしく色々とヒントをくれました。% brew info mysql mysql: stable 8.0.22 (bottled) Open source relational database management system https://dev.mysql.com/doc/refman/8.0/en/ Conflicts with: mariadb (because mysql, mariadb, and percona install the same binaries) percona-server (because mysql, mariadb, and percona install the same binaries) /usr/local/Cellar/mysql/8.0.22_1 (294 files, 296.5MB) * Poured from bottle on 2020-12-13 at 08:50:29 From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/mysql.rb License: GPL-2.0 ==> Dependencies Build: cmake ✘ Required: openssl@1.1 ✔, protobuf ✔ ==> Caveats We've installed your MySQL database without a root password. To secure it run: mysql_secure_installation MySQL is configured to only allow connections from localhost by default To connect run: mysql -uroot To have launchd start mysql now and restart at login: brew services start mysql Or, if you don't want/need a background service you can just run: mysql.server start ==> Analytics install: 85,291 (30 days), 236,256 (90 days), 823,613 (365 days) install-on-request: 83,799 (30 days), 231,594 (90 days), 798,215 (365 days) build-error: 0 (30 days)どうやらmariadbとpercona-serverがconflictを起こしていることとcmakeがインストールされていないことが悪いのかなと考えました。そこでまず、簡単な方のcmakeをインストールを試してみました。その後再度brew info mysqlを叩いてみました。
% brew install cmake % brew info mysqlとりあえず、cmakeの✖️からレ点に変わったので進歩しました。一応、mysqlがinstallできるか確認してみました。
% sudo gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'相変わらずインストールができないことが確認できました。今度はしっかりとエラーを確認してみます。
Building native extensions. This could take a while... ERROR: Error installing mysql2: ERROR: Failed to build gem native extension. current directory: /Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/ext/mysql2 /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/bin/ruby -I /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0 -r ./siteconf20201218-6446-1qkv5ft.rb extconf.rb checking for rb_absint_size()... yes checking for rb_absint_singlebit_p()... yes checking for rb_wait_for_single_fd()... yes ----- Using mysql_config at /usr/local/bin/mysql_config ----- checking for mysql.h... yes checking for errmsg.h... yes checking for SSL_MODE_DISABLED in mysql.h... yes checking for SSL_MODE_PREFERRED in mysql.h... yes checking for SSL_MODE_REQUIRED in mysql.h... yes checking for SSL_MODE_VERIFY_CA in mysql.h... yes checking for SSL_MODE_VERIFY_IDENTITY in mysql.h... yes checking for MYSQL.net.vio in mysql.h... yes checking for MYSQL.net.pvio in mysql.h... no checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes checking for SERVER_QUERY_NO_GOOD_INDEX_USED in mysql.h... yes checking for SERVER_QUERY_NO_INDEX_USED in mysql.h... yes checking for SERVER_QUERY_WAS_SLOW in mysql.h... yes checking for MYSQL_OPTION_MULTI_STATEMENTS_ON in mysql.h... yes checking for MYSQL_OPTION_MULTI_STATEMENTS_OFF in mysql.h... yes checking for my_bool in mysql.h... no ----- Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load ----- ----- Setting libpath to /usr/local/Cellar/mysql/8.0.22_1/lib ----- creating Makefile current directory: /Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/ext/mysql2 make "DESTDIR=" clean current directory: /Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3/ext/mysql2 make "DESTDIR=" compiling client.c compiling infile.c compiling mysql2_ext.c compiling result.c compiling statement.c linking shared-object mysql2/mysql2.bundle ld: warning: directory not found for option '-L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.Internal.sdk/usr/local/lib' ld: library not found for -lssl clang: error: linker command failed with exit code 1 (use -v to see invocation) make: *** [mysql2.bundle] Error 1 make failed, exit code 2 Gem files will remain installed in /Library/Ruby/Gems/2.6.0/gems/mysql2-0.5.3 for inspection. Results logged to /Library/Ruby/Gems/2.6.0/extensions/universal-darwin-19/2.6.0/mysql2-0.5.3/gem_make.outlibpathを通した方がいいのかな?と思い通してみます。(この解釈は間違っていることにあとで気付きます)
% export LIBRARY_PATH=/usr/local/Cellar/mysql/8.0.22_1/lib % sudo gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'しかし、エラー内容が変わらず。ここでCreating Makefileの後にゴニョゴニョ書いていることからgem installするときに何かしらのMakefileを実行していて、その途中でld: liblary not found for -lsslで失敗してインストールが失敗しているのではないかと考えました。(間違っていたらすみません。)
ここで白旗を上げて、同じ問題に当たっている人を探すことにしました。すると以下の優良な記事がヒットしました!
mysql2 gemインストール時のトラブルシュートここをみてもらえば最初から解決できる話でしたww
ここまで読んでくれた人は申し訳ございませんでした。Qiitaを書きながらなのリアルなエラー解決のストーリーとなっているのでご了承ください。意訳すると、LIBRARY_PATH=/usr/local/Cellar/mysql/8.0.22_1/libにパスを設定したけど、お目当てのライブラリーが見つからないためにlibrary not foundになっていて、--with-cppflagsと--with-ldflagsを指定することでちゃんとライブラリーが見つけれるようになって、インストールできるよになるという話です。正確には元記事を参考にしてください。
解決方法
% brew info openssl For compilers to find openssl you may need to set: export LDFLAGS="-L/usr/local/opt/openssl/lib" export CPPFLAGS="-I/usr/local/opt/openssl/include" % sudo gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/' -- --with-cppflags=-I/usr/local/opt/openssl/include --with-ldflags=-L/usr/local/opt/openssl/lib % bundle installお疲れ様でした。もしrails newしたけど、mysqlのインストールでつまづいた人がいれば参考にしていただければ幸いです。
- 投稿日:2020-12-18T09:49:51+09:00
ランダム性のある電子音楽をRubyで作る
本記事は iCARE Advent Calendar 2020 の18日目です。
音楽をRubyで書く
「音楽作ってました!」「DAW触ってます!」
これはエンジニア界隈では本当にあるあるだと思います。なぜでしょう 笑
自分もその一人で、エレクトロニカやIDMと言われるジャンルの曲を作っています。さて、皆様はSonic Piをご存知でしょうか。
このツールを使えばRubyで音楽が作れるということを知り試したところ、すごく面白くて便利なツールだと感じました。
そしてこの記事はこれだけ抑えておけばSonic Piでそれっぽいものが作れるよというものです。Sonic Pi
上記の通り、Sonic Piをダウンロードして使用します。
概要についてはQiitaで検索すればいろいろと出るので詳しい紹介はしませんが、
この環境でRubyを書き、実行することで音を奏でることができます。
Sonic Piは電子音楽向き
電子音楽や実験音楽は綿密に計算された楽曲よりも、意図的に偶然を生んで生成した音を利用した楽曲が多いように感じます。
妙な立体感、不思議な音、ぐちゃぐちゃなのに心地良いリズム、ノイズなど、、
そしてSonic Piはそんなランダム性のある音を簡単に生成してくれます。
最高です。はい。グリッチなビートを作ってみた
グリッチとは
デジタル装置のエラー、そのために生じるノイズ、またはそれらを利用して作品を制作する手法のこと。この手法は1990年代半ばの音楽に現われ、偶発的で小さな欠陥を意味する「グリッチ」という語で呼ばれ始める。
https://artscape.jp/artword/index.php/%E3%82%B0%E3%83%AA%E3%83%83%E3%83%81環境構築・基本動作
本記事では省略します。
こちらの記事に細かく書かれていれるので、気になる方はどうぞ。
とても分かりやすくまとめられています。ソース
# 四つ打ちのキック live_loop :kick do use_bpm 60 sample :bd_haus, rate: 0.8, release: 0.4, amp: 0.5 sleep 1 # 1拍休む end # 上とずらして同様のキックを打つ live_loop :kick2 do sleep 0.75 sample :bd_haus, rate: 0.5, amp: 0.7 end # アンビエンスなもわっとした音で雰囲気をつくる live_loop :ambi_dark do sample :ambi_dark_woosh, rate: 0.5, finish: 0.2 sleep 0.5 end # パーカッション live_loop :perc do # ディストーションエフェクトをかける with_fx :distortion do sample :glitch_perc1, rate: 0.2, finish: 0.05, amp: 0.05 sleep 1.5 end end # パーカッション2 live_loop :perc2 do # rateをランダム化して1再生ごとの表現を変える sample :glitch_perc5, rate: rrand(0.1, 1.5), amp: 0.1, slice: 0.1 sleep 0.125 end解説
やっていることは単純で、
1. サンプルを選ぶ
2. rateを設定する
3. amp, releaseなどの調整
4. 肝となる要素にrrandを設定する
くらい。サンプルを選ぶ
Sonic Piはデフォルトで多くの音サンプルを用意しており、
sample hoge
とするだけで簡単にサンプルを呼び出すことができます。rate
rateは音の速度を変更できます。
1が標準の長さ、0.5にすると音は標準の半分の速度で再生されます。
半分の速度で再生されるとゆっくりになるのはもちろん、音程も低くなります。
グラスをお箸で叩くとチーン
と鳴りそうですが、sample :chine, rate: 0.1とすれば
じいいいいぃぃぃぃぃぃぃぃぃぃぃぃぃぃんんんん
と鳴るでしょう。
またマイナスの値を設定すると電子音楽家の大好物、逆再生になります。sample :chine, rate: -1
んんんぅぅぅぅぃぃぃぃいいい↑↑
といった感じでしょうか。表現が下手ですみません。
シンバルをrate: -1すればリバースシンバルになるイメージです。音色や音程を一気に変えてくれるrateオプション、素晴らしいです。
amp, release
ampは音量です1が標準、0.5にすると半分の音量になります。
releaseは俗にいうADSR(Attack, Decay, Sustain, Release)のReleaseで、減衰音を指します。
余韻に近いでしょうか。
上のソースの最初のところで、releaseを0.4に設定しています。
グリッチ感を出す他の要素の邪魔をしないためにも、余韻を減らして切れ味の良いキックを目指しました。# 基本のキック live_loop :kick do use_bpm 60 sample :bd_haus, rate: 0.8, release: 0.4, amp: 0.5 sleep 1 # 1拍休む end肝となる要素にrrandを設定する
名前から察することができるように、rrandオプションで設定した数値をランダム化することが可能です。
ソースの最後、パーカッション2のrateで使用しています。# パーカッション2 live_loop :perc2 do sample :glitch_perc5, rate: rrand(0.1, 1.5), amp: 0.1, slice: 0.1 sleep 0.125 end
rate: rrand(0.1, 0.5)
とすることで1小節でループする度に0.1から0.5の間の数値をランダムにセットしてくれるようになります。
こうすることでそれぞれの小節で音に変化を与えられる・・・
つまり機械的な単調な音から生きた音に昇華することができます。
最高です。ソースを実行した結果
1分ほど録音し、soundcloudにアップしました。(イヤホン推奨)
https://soundcloud.com/tenten1105/sonic_pi-beat我ながらなかなか良いものができたと思います!
なんとSonic Piは録音機能もついていて、実行したデータをその場ですぐwavで書き出してくれるところも素敵です。あとがき
あれだけのコードでここまでできるSonic Piは本当に素晴らしいです。
今回はsampleを使用しましたが、自身のDAWで作成したデータや環境音を録音したものを使用してrrandで加工後、DAWに戻して制作なんてことも勿論できます。
可能性は無限大ですね!
ありがとう、Ruby。ありがとう、Sonic Pi。
- 投稿日:2020-12-18T09:48:03+09:00
【Rails】多対多の作成
本投稿の目的
・Railsについての議事録です。
学習に使った教材
Udemyの以下2つの教材を参考にまとめました。
・"はじめてのRuby on Rails入門-RubyとRailsを基礎から学びWebアプリケーションをネットに公開しよう"
・"フルスタックエンジニアが教える 即戦力Railsエンジニア養成講座"
○多対多の関係とは?
・複数のcolumnが相互に紐づいたmodel同士の関係
・例):掲示板アプリのboard とtagのような関係 (投稿内容は異なるがtagが同じような投稿は多対多の関係)○紐付けのポイント
・中間テーブルを用意する (本投稿での説明では,model(中)と定義)
・中間テーブルは [:id,多1_id,多2_id] のみのシンプルな構造○紐付け設定方法
①model多1とmodel多2を普通にgenerateする
rails g <model(多)1> <column情報> rails g <model(多)2> <column情報>②中間テーブルをgenerateする
rails g model <model(中)> <model(多)1>:references <model(多)2>:references・中間テーブルは通常は "model(多)1_model(多)2_relations" という名前が一般的
③migrationを実行
rails db:migrate・①②で設定したmodelがdbのtableに反映される
④中間テーブルのmodelへの記述
model(中).rbclass model(中) < ApplicationRecord belongs_to :model(多)1 belongs_to :model(多)2 end・この記述はgenerate時に自動記述
・belongs_toが2つ設定済みのためノータッチでOK⑤moel(多)1のmodelへの処理
model(多)1.rbclass model(多)1 < ApplicationRecord has_many :<model(中)s> has_many :<model(多)2>, through: :<model(中)s> end・
through: :<model(中)s>
は,2つのmodel間にmodel(中)を経由するという意味
・(model(多)1 → model(中) → model(多)2 という意味)
・中間テーブルを経由することをここで記述して置く必要がある⑥model(多)2のmodelへの処理
model(多)2.rbclass model(多)2 < ApplicationRecord has_many :<model(中)s> has_many :<model(多)1>, through: :<model(中)s> end・model(多)1と同様に2つのhas_manyを設定