- 投稿日:2019-12-19T23:56:25+09:00
[WIP]ActiveRecordを使い、複数テーブルへBULK INSERTする方法
目的
- Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
- 今後、railsを学ぶ方に向けての参考に役立てる。
前提条件
実行環境
- Ruby 2.5.1
- Rails 5.2.3
- MySQL 5.7
gem
想定
- 大量のデータ(CSVファイル)をseedで一括で取り込みたい
手順
- CSVファイルを設置する
- seeds.rbにCSVをBULK INSERTするコードを記述
- 各modelにオプションを設定
rails db:seed(完)1. CSVファイルを設置する
db ├── data │ ├── hoge.csv ← 初期データとして取込みたいCSVファイルを設置 │ └── fuga.cs ← 初期データとして取込みたいCSVファイルを設置 ├── migrate │ ├── 20191118191522_create_huges.rb │ └── 20191119060537_create_fugas.rb ├── schema.rb └── seeds.rb2. seeds.rbにCSVをBULK INSERTするコードを記述
db/data/seeds.rb# csvライブラリを読み込みます。 require 'csv' # 冪等性を担保するためトランザクション処理する # 冪等性とは「同じ操作を何度繰り返しても、同じ結果が得られる性質」のこと ActiveRecord::Base.transaction do import = ->(klass, file_path) do options = { encoding: "UTF-8", headers: true, header_converters: klass.csv_header_converters, converters: klass.csv_converters } # 登録件数が多いことを想定して念のため1000件ずつ処理を行うように設定 CSV.read(file_path, options).each_slice(1000) do |rows| values = rows.map(&:to_h).map { |row| klass.new(row) } klass.import! values, validate: true end end import.(Hoge, Rails.root.join('db', 'data', 'dataset_50.csv')) import.(Fuga, Rails.root.join('db', 'data', 'house_data.csv')) end3. 各modelにオプションを設定
app/models/hoge.rbclass Hoge < ApplicationRecord belongs_to :fuga validates :label, :fuga_id, :year, :month, presence: true class << self def csv_header_converters -> (name) { { ID: :id, Label: :label, Fuga: :fuga_id, Year: :year, Month: :month, }[name.to_sym] || raise } end def csv_converters nil end end endapp/models/fuga.rbclass Fuga < ApplicationRecord has_many :hoge validates :firstname, :lastname, :city, presence: true validates :has_child, inclusion: { in: [true, false] } class << self def csv_header_converters -> (name) { { ID: :id, Firstname: :firstname, Lastname: :lastname, City: :city, Num_Of_People: :num_of_people, Has_Child: :has_child }[name.to_sym] || raise } end def csv_converters -> (field, field_info) { if field_info.header == :has_child field == 'Yes' else field end } end end end4.
rails db:seed(完)bashrails db:seed関連URL
所感
久しぶりに書いた。業務でCSVインポートの処理を実装するの何気に多い。
後で再インプットも兼ねて解説コメント丁寧に書く。
気力があれば過去記事のメンテナンスしていく
- 投稿日:2019-12-19T23:56:25+09:00
[WIP]ActiveRecordを使って複数テーブルへBULK INSERTする方法
目的
- Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
- 今後、railsを学ぶ方に向けての参考に役立てる。
前提条件
実行環境
- Ruby 2.5.1
- Rails 5.2.3
- MySQL 5.7
gem
想定
- 大量のデータ(CSVファイル)をseedで一括に取り込みたい
手順
- CSVファイルを設置する
- seeds.rbにCSVをBULK INSERTするコードを記述
- 各modelにオプションを設定
rails db:seed(完)1. CSVファイルを設置する
db ├── data │ ├── hoge.csv ← 初期データとして取込みたいCSVファイルを設置 │ └── fuga.csv ← 初期データとして取込みたいCSVファイルを設置 ├── migrate │ ├── 20191118191522_create_huges.rb │ └── 20191119060537_create_fugas.rb ├── schema.rb └── seeds.rb2. seeds.rbにCSVをBULK INSERTするコードを記述
db/data/seeds.rb# csvライブラリを読み込みます。 require 'csv' # 冪等性を担保するためトランザクション処理する # 冪等性とは「同じ操作を何度繰り返しても、同じ結果が得られる性質」のこと ActiveRecord::Base.transaction do import = ->(klass, file_path) do options = { encoding: "UTF-8", headers: true, header_converters: klass.csv_header_converters, converters: klass.csv_converters } # 登録件数が多いことを想定して念のため1000件ずつ処理を行うように設定 CSV.read(file_path, options).each_slice(1000) do |rows| values = rows.map(&:to_h).map { |row| klass.new(row) } klass.import! values, validate: true end end import.(Hoge, Rails.root.join('db', 'data', 'hoge.csv')) import.(Fuga, Rails.root.join('db', 'data', 'fuga.csv')) end3. 各modelにオプションを設定
app/models/hoge.rbclass Hoge < ApplicationRecord belongs_to :fuga validates :label, :fuga_id, :year, :month, presence: true class << self def csv_header_converters -> (name) { { ID: :id, Label: :label, Fuga: :fuga_id, Year: :year, Month: :month, }[name.to_sym] || raise } end def csv_converters nil end end endapp/models/fuga.rbclass Fuga < ApplicationRecord has_many :hoge validates :firstname, :lastname, :city, presence: true validates :has_child, inclusion: { in: [true, false] } class << self def csv_header_converters -> (name) { { ID: :id, Firstname: :firstname, Lastname: :lastname, City: :city, Num_Of_People: :num_of_people, Has_Child: :has_child }[name.to_sym] || raise } end def csv_converters -> (field, field_info) { if field_info.header == :has_child field == 'Yes' else field end } end end end4.
rails db:seed(完) ?bashrails db:seed関連URL
所感
久しぶりに書いた。業務でCSVインポートの処理を実装するの何気に多い。
後で再インプットも兼ねて解説コメント丁寧に書く。
気力があれば過去記事のメンテナンスしていく
- 投稿日:2019-12-19T23:43:59+09:00
Rspecつまづきポイント!
この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc. Advent Calendar 2019 20日目の記事です。
はじめに
ここ一年ほど、社内で自動テスト推進係をしております@ysysysysがお送りいたします。
社内の各プロジェクト、特にRailsプロジェクトでRspecを書きましょうと皆さんとともに日々頑張っております。
今回はRspecを書いていく中で個人的につまづきポイントだったなと思うところをいくつかピックアップしてその対応をご紹介しようかと思います。その1 let と let!
Rspecではletという機能があります。Spec内ではインスタンス変数で定義するよりもlet(:hoge)と定義するのが一般的なようです。
letをつかえば、その内容はメモリーされ、exampleのたびに再生成するコストが減るとのこと。
ここでつまづいたのが、このletが遅延評価するという特性による挙動です。user_factory.rbFactoryBot.define do factory :user do first_name { '太郎' } last_name { '山田' } email {'testuser@example.com'} end endこのようにFactoryBotでUserを作り、そのModelのテストを行うとします。
user_spec.rbdescribe '#full_name' do let(:user) { create(:user) } it '山田太郎を返す' do expect(user.full_name).to eq '山田太郎' end endところがこれでは正常に動作しないです。
letが評価されるタイミングはuserを呼び出した時です。なのでfull_name実行時にはuserは作られておらずうまくいかないのです。user_spec.rbdescribe '#full_name' do let!(:user) { create(:user) } it '山田太郎を返す' do expect(user.full_name).to eq '山田太郎' end endletの後ろに!をつけることで、itの中身が実行される前に中身が作られるようにするとうまくいきます。
その2 サブドメインのテスト
こんな感じのルートがあった時に、サブドメイン配下のRequest specを書きたい場合はどうすればいいかハマりました。
routes.rbconstraints subdomain: 'hoge' do get '/' => subdomain#index end正解はこちら⬇︎
subdomain_request_spec.rbdescribe 'GET index' do it "200を返すこと。" do headers = { "Host" => "hoge.example.com" } get '/', headers: headers expect(response).to have_http_status(:ok) end endこのようにgetする際のheaderにHostを引っ付けてあげればいけるんですね〜
確認するとちゃんとサブドメインが取れています。pry(#<RSpec::ExampleGroups::SubdomainController>) request.subdomain => "hoge"その3 モバイルのテストをしたい!
テンプレートをPCかスマホで出し分けている時、スマホでもテストをしたい場合はどうすればいいかです。
結論から言うとこれもheadersをいじります。request_spec.rbdescribe 'GET index' do it "スマホで200を返すこと" do headers = { "Host" => "HTTP_USER_AGENT" => "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X)" } get '/', headers: headers expect(response).to have_http_status(:ok) end endこれでモバイルだけおかしい!みたいなこともなく安心。
その4 セッションを持たせておきたい
ログイン先のページをテストする際などセッションがあることを前提とした部分をてすとしたいときです。
こうしましょうrequest_spec.rbdescribe 'GET mypage' do setup do post "/login", params: {email: 'test@example.com', pass: 'testtest' } end it "200を返すこと" do get '/mypage' expect(response).to have_http_status(:ok) end endsetupブロックで事前に/loginにpostしておくことでセッションを生成しておくことができます。
そのあとにexampleを書いていけばOK!その5 CarrierWaveを使っているところのテスト
Carrierwaveを使っていて画像をアップロードする部分のテストはどうすればいいの!
upload_request_spec.rbbefore(:each) do allow_any_instance_of(ImageUploader).to receive(:store!).and_return(true) end let(:params) do { image: { file: Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/images/test.jpg')), } } end it 'Imageがアップロードされている。' do post '/upload', params: params expect(Image.count).to eq 1 endテスト用の画像ファイルをプロジェクト配下のどこかに置いておいて、読み込んであげればOKです。
私の場合は画像をS3にアップしていたのでその処理をスキップするためにUploaderのstore!メソッドをスタブしました。
本当は環境ごとに適切に分けておいた方がいいんでしょうが、、まとめ
Rspecを書いていく中で、個人的に印象に残っているつまづきポイントをざっと紹介しました。
こんな感じでつまづくところはつまづきつつ、日々テストコード書いてプロダクトの品質向上に努めております。お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はエイチームグループ採用ページよりご応募くださいませ。Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム、社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能でございます。
よろしくお願いいたします!
明日は
明日は弊社技術開発部の貴公子@nao_70さんの登場です。お楽しみに。
- 投稿日:2019-12-19T23:43:11+09:00
ド素人が超簡単なwebアプリ作ってみた2。
第2段はRailsで
今年10月の半ばに完成。第一弾がから数ヶ月。細く長く学んできました。
見てくれしょぼくてもこれ引っさげて現在転職活動中。
ポートフォリオは有効と聞いたが未経験での転職だとそもそも見られない事が多いね。意欲は認めてくれる。やっぱ職歴が全てってかんじ。何するアプリ?
普段業務でLPのコラム書いたりするのでテキストと画像を投稿するだけのシンプルなアプリ。
実装機能
・記事投稿、記事更新、記事削除機能
・ユーザー作成、更新機能
・検索、ソート機能技術スタック
・Webフレームワーク:Ruby on Rails、Bootstrap4
・PaaS:heloku見どころ
ログイン機能はGem使わずに実装しました。
URL
・デプロイ先
https://crudsample20190928.herokuapp.com/・Githubのリポジトリ
https://github.com/rrrhtm/curdsampleおわりに
言語やフレームワークにこだわらないで色々面接受けていこうと思う。
未経験者の戦闘力がインフレしているらしいので不安だけども。
- 投稿日:2019-12-19T23:38:56+09:00
Ruby 学習アウトプット2
はじめに
初学者のrubyシルバー取得に向けての学習アウトプットです。
文章もただただ、タコ殴りしてるだけなので、温かい目で見守っていただけると幸いです。今回の教材
REx
REX??恐竜???
T-REXっていうバンドもいますよね。否!!
https://www.ruby.or.jp/ja/certification/examination/rex
受ける度に問題の内容が変わります。8割共通で残りの2割が入れ替わる。
もちろん以前の問題を再び受け直すことも可能。解説も割と書いてある。
スマホからも受けられて便利。そして無料。
githubアカウントとの連携が必要。やってみて
100点中、52点。。。。。まだ先は長い。
ということで復習しましょう復習(復讐)
- 次のコードのXXXXに記述するとエラーが発生するコードはどれですか?
puts XXXX "0x90".hex #=>144 '90' #=>"90" 0b2 #=>numeric literal without digits(エラー) '80'.oct #=>00xは16進数を表すプレフィックスです。
String#hexは文字列を16進数で解釈して、整数で返します。16進数で解釈できない場合は0を返します。
0bは2進数を表すプレフィックスです。
今回の選択肢0b2は2進数ではありません。0b10が正しい定義です。
String#octは文字列を8進数で解釈して、整数で返します。8進数で解釈できない場合は0を返します。
今回の選択肢”80”は8進数ではありません。"100"が正しい定義です。上記解説なのですが、○進数に関して全然分からないというか、義務教育の過程で習ったのかどうかも、、、ってことなのでここは分かるようにしときたいです。
この解説で理解できなかったので○進数について学びなおします。とほほ。
- 次のプログラムを実行するとどうなりますか?
$val = 0 class Count def self.up $val = $val + 1 $val == 3 ? true : false end end [*1..10].any? do Count.up end p $val #以下のどれが表示されるか 10 1 0 3答えは3です。
[*1..10].any?こちらは[1..10]だとRangeクラスで範囲になりますが、[*1..10]で[1,2,3,4,5,6,7,8,9,10]のArrayクラスの配列になります。そしてany?でブロックが真が返したときは繰り返しを中断してtrueを返します。ブロックの戻り値がすべて偽であるとき、あるいはレシーバが空であるときにはfalseを返します。
繰り返しが止まるのは3回目の繰り返し、つまり$valが3になった時点です。この問題のプログラムを実行すると、3が表示されます。
- 次のコードを実行するとどうなりますか
hoge = "a".to_h puts hoge.class Stringと表示される Arrayと表示される Hashと表示される エラーになる答えはエラーになるです。
undefined methodto_h' for "a":String (NoMethodError)`
- 次のコードを実行するとどうなりますか?
10.times{|d| print d == 3..d == 5 ? "T" : "F" } シンタックスエラーとなる FFFFFFFFFF と表示される FFFTFTFFFF と表示される FFFTTTFFFF と表示される答えは
FFFTTTFFFF と表示されるなのですが、最初解説見ても全然分からなくてですね。
特にd == 3..d == 5この部分がd == 3..d????そんで3..d == 5??????
となっていたのですが、実はこれ(d == 3)..(d == 5)として考えれば、なんの問題もなく理解できました。範囲式でググればOKです。
やってみて
1回目50点、2回目は80点超えではありました。
今の所、自分ができていないのでは○進数系の問題、ヒアドキュメント、Dateクラス、IOクラスとりあえず、そのへんを固めていきたいです。
以上になります。
- 投稿日:2019-12-19T20:44:08+09:00
これから学ぶ言語についてのまとめ
これから学ぶ言語について
これからプログラミングを学ぶにあたり、HTML,CSS,Javascript,Ruby,Ruby on Railsの5つの言語について調べてみました。
(間違いがございましたらご指摘頂けますと幸いです!)HTML
Hyper Text Markup Language(ハイパーテキスト・マークアップ・ランゲージ)
Webページを作るための最も基本的なマークアップ言語のひとつです。
Webページの文書構造を形作るための言語で、普段観ているWebページのほとんどが、HTMLで作られています。
Hyper Text(ハイパーテキスト)とは、ハイパーリンクを埋め込むことができる高機能なテキストです。
HTMLにはハイパーリンク機能で関連する情報同士を結びつけて情報を整理するという特徴があります。
Markup(マークアップ)とは文書の各部分が、どのような役割を持っているのかを示すということです。
コンピュータに理解できるように文書の構造を定義することが、HTMLの最も重要な役割で、
この際目印をつけるための記号として使用されるのがHTMLタグです。HTMLには「HTML」「XHTML」「HTML5」など、いくつか種類があります。
CSS
“Cascading Style Sheets” (カスケーディング・スタイル・シート)スタイルシート
スタイルシート言語の1つで、文書構造にデザインを施し見栄えを整える役割を担っています。
CSSは、HTMLと組み合わせて使用する言語です。
WEBページ=HTML(文書構造)+CSS(見栄え)で構成されています。ウェブページをどのようなスタイルで表示・出力・再生するかについて指定することができます。
JavaScript
JavaScript(ジャバスクリプト)
- JavaScriptとは、現在、世界中のデベロッパーの間で最も人気のあるプログラミング言語の1つ。私たちはみんなJavaScriptのヘビーユーザー。普段から1日に何十回も使用しています。
特徴
JavaScriptはスプリクト言語のためJavaやC言語と違いコンパイルする必要がありません。
JavaScriptとは、動的にコンテンツを更新したり、マルチメディアを管理したり、その他多くのことができるプログラミング言語
JavaScriptはユーザー側のWebブラウザとWebサイト・ウェブサービスの相互間のやりとりを円滑にします。
JavaScriptのコードはブラウザがWebページにアクセスしたときにダウンロードされ、画面上に表示されるとJavaScriptのコードが実行されて次に実行するべき動作がもしあればブラウザに指示します。
Javascriptで出来ること
フォームのチェック
フォームに入力されたテキストデータをサーバに送る前にチェックできますイベント処理
キーボードやマウスによる操作によってWebページの動的な動きを実現できます。テキストの処理・加工
テキストのスタイルを変化させたり、文字を抜き出したり、テキスト回りの加工も可能です。Cookieの処理
Cookieを作ったり、逆にユーザーのCookieデータを読み込んだりできますウインドウの操作
出現するウィンドウの位置やサイズ、スクロールさせるなど、さまざまな動きを指定できます。ほかにも、JavaScriptでできることはたくさんあります。
Ruby
Ruby(ルビー)
- Rubyは、『まつもとゆきひろ』により開発されたオブジェクト指向のスクリプト言語。 日本で開発されたプログラミング言語として初めて国際規格に認証されました。
- 「書くのが楽しい」「気持ちよくかける」とよく言われます。
主な特徴
Rubyは他の言語と比べると記述量が少なくて済みます。
JavaやPHPといった他の言語同様、Rubyはオブジェクト指向なプログラミング言語です。
Rubyはスクリプト言語であり、JavaやC言語と違いコンパイルする必要がありません。
Rubyで出来ること
Webアプリ開発
Ruby on RailsというWebアプリ開発用のフレームワークを利用することで、比較的簡単にオリジナルのアプリケーションを制作することができます。スマホアプリ開発
Webアプリケーションだけでなく、RUBYMOTIONを利用することで、スマホアプリ開発をすることが可能です。スクレイピング
Webサイトから情報を取得し加工するという意味です。Ruby on Rails
Ruby on Rails(ルビーオンレイルズ)RoR 又は Rails
Ruby on RailsはオープンソースのWebアプリケーションフレームワークである。
Railsの基本理念は「同じことを繰り返さない」(DRY:Don't Repeat Yourself)と「設定より規約」(CoC:Convention over Configuration)である。
Ruby on Railsとは、Rubyで構築された、Webアプリケーション開発のための「フレームワーク」です。
Ruby on Railsでは、MVCアーキテクチャ(MVCモデル)という設計法に基づいて開発を行います。
気になった調べた言葉
5つの言語を調べているうちに気になって調べた言葉です。
マークアップ言語
組版指定に使われる言語で視覚表現や文章構造などを記述するための形式言語。(HTML、XML、SGML)
スタイルシート言語
構造化文書の見た目を記述するコンピュータ言語。見栄えと構造を分離するという目的で提唱された。スタイルシートとはCSSのことを指す。
コンパイラ言語
高速で動作する代わりに記述が面倒。 ソースコードを事前にコンパイルして、先に機械語に変換しておく。(C、C++、Java、C#など)
スクリプト言語
低速で動作する代わりに短い記述で処理を実現。 ソースコードを逐次機械語に翻訳しながら実行する。 (Python、Perl、PHP、Ruby、JavaScriptなど)
コンパイラ方式とインタープリタ方式
プログラミング言語を変換するタイミングによって,インタープリタ方式とコンパイル方式の2つの型に分類できます。
インタープリタ方式
ソースコードをその場で順を追って解釈する形式をいいます。(Javascript,Ruby)メリットは作成したソースプログラムをただちに実行できるという点。
コンパイラ方式
ソースプログラムをいったん機械語に翻訳し,その機械語になったプログラムを実行する方式です。メリットは実行速度が速い点。
オブジェクト指向プログラミング
「モノ」を組み立てるように表現して、コンピュータに動作をさせること。
Cookie
Web閲覧時に生成されるCookieデータは、サイトで登録したIDやパスワード、ページを見たときの日時や回数を記録するデータです。
MVCとは
Model(データを扱う)の頭文字Mと、View(ページの見た目)の頭文字V、Controller(ModelとViewとの連携を行う)の頭文字Cを取ってMVCと呼ばれています。
フレームワークとは
Webアプリケーションを簡単に開発できるようにする骨組みのこと。
- 投稿日:2019-12-19T20:42:28+09:00
配列とハッシュ
配列とハッシュ
日々のRubyの学習で学んだことを備忘録としてつぶやいていきます。
配列
配列とは?
通常の変数を1つの箱と捉えれば、配列は、仕切りがある箱のようなもの。
配列を用いることで1つの変数で複数の値を管理することができる。
配列は、要素を順番で管理する。
◯番目といった順番を表す数字のことをインデックスと言う。インデックスは0番目から始めることに注意!!
配列の書き方
ary.rbary = ["10","20","30"] # 3つの値を1つの変数に格納しているハッシュ
ハッシュとは?
ハッシュも配列と同じで、複数の値を一括で管理したいときに使われる。
配列は、順番で管理していたことに対し、ハッシュは、名前で管理することができる。
ハッシュとは、キー名: 値の形式で値を取ってくることができる。ハッシュの書き方
hash.rbhash = {name: "reo", age: 20, country: "japan"} # キー名: バリューの形式で値を格納している配列のハッシュの知識を組み合わせたコード
work.rbstudents = [ {name: "田中", Japanese: 85, Math: 62, English: 77}, {name: "佐藤", Japanese: 90, Math: 40, English: 98}, {name: "ボブ", Japanese: 2, Math: 50, English: 100} ] students.each do |student| line = "--------------" puts line sum = student[:Japanese] + student[:Math] + student[:English] puts "解答者: #{student[:name]}さん 国語: #{student[:Japanese]}点 数学: #{student[:Math]}点 英語: #{student[:English]}点 合計: #{sum}点" endターミナル出力結果----------------- 解答者:田中さん 国語:85点 数学:62点 英語:77点 合計:224点 ----------------- 解答者:佐藤さん 国語:90点 数学:40点 英語:98点 合計:228点 ----------------- 解答者:ボブさん 国語:2点 数学:50点 英語:100点 合計:152点
- 投稿日:2019-12-19T20:05:59+09:00
RailsロガーからStackdriverに切り替える
はじめに
- 当該記事はたたき台のため、追記予定
システム情報
- Ruby 2.6.5
- Rails 5.2.3
- bundler 1.17.2
- puma
- rbenv local環境下
Terminal$ rbenv versions system 2.4.1 * 2.6.5 (set by /Users/%user%/%repository%/.ruby-version) $ bundle exec rails -v Rails 5.2.3Stackdriverに切り替える
これまでの構成
- Rails.loggerのformatterを
config gemで環境毎に設定(或いはsettings.ymlでも良い)config/settings/development.yml…途中省略… # logging setting # fatal システムクラッシュ(システムエラー) # error アプリケーションエラー # warn 警告 # info プログラム通知 # debug 開発者向けの情報 logger: formatter: 'proc {|severity, datetime, progname, msg| "severity:[#{severity.rjust(5, " ")}]\tdatetime:#{datetime.strftime("%Y/%m/%d %H:%m:%S")} pid:#{$$}\tmessage:#{msg}\n"}'production.rb|development.rbconfig.log_formatter = eval Settings.logger.formatterLogseverity:[ INFO] datetime:2019/12/19 00:00:00 pid:20026 message:Started GET "/api_v2/users?first_name=ほげお&gender=male&zip_code=100-0001" for ::1 at 2019-12-19 00:00:00 +0900 severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: (8.5ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: ↳ vendor/bundle/ruby/2.6.0/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98 severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: (8.2ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: ↳ vendor/bundle/ruby/2.6.0/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98 severity:[ INFO] datetime:2019/12/19 00:00:01 pid:20026 message:Processing by ApiV2::UsersController#index as */*これに
logrotateの組み合わせでも全く構わないとは思うのですが、今回はこれをStackdriverに切り替えたいと思います。Stackdriverとは
- モニタリング、ロギング、および診断機能を提供する管理ツールスイート
- 詳しくはGitHub のドキュメント
- 用途を大きく分類すると、Stackdriver Logging AgentとLogging ライブラリとなる
- Stackdriver Logging AgentとLogging ライブラリの導入どちらかによってStackdriverの運用方法が大きく異なる
スイートに含まれるパッケージ・ライブラリ
Stackdriver Debugger
- コードの動作を調査し、デバッグを行える
- gem を組み込んだら、Cloud Console に移動し、コードをアップロードします(または、Stackdriver Debugger でリポジトリ内のコードを指定します)。すると、実行中のアプリケーションの変数値とスタックトレースを含むスナップショットを取得
- 実行中のアプリケーションに任意のログ文を追加することも可能
Stackdriver Error Reporting
- 例外を検出して報告する Google Cloud ツール
- アプリケーションのクラッシュを捉え、それを論理的にグループ化して通知してくれる
- UI(エラー管理インターフェース)に整理してわかりやすく表示
- UI には、エラーのスタックトレースや、各クラッシュのログおよび分散トレースへのリンクが表示される
- グループ化されたバグ データベースから発生したバグを関連づけることができる
Stackdriver Trace
- アプリケーションのパフォーマンス モニタリングと分散トレーシングを行う
- リクエストがデータベースに到達し、ビューがレンダリングされ、アプリケーション ロジックの実行に要した時間が自動的に表示される
- リクエストがマイクロサービス アーキテクチャでどのように処理されるかを表示
- レイテンシの時系列の傾向に関する詳細レポートを提供
- 最新リリースへの更新後、アプリケーションに遅延はないかチェックできる
Stackdriver Logging
- GA
- ログデータのフルマネージドサービス
- ログデータのリアルタイム分析に対応
- fluentd ロギング エージェントとともに利用される
- Container Engine を使っていなくても利用できる
- Ruby Logger や Rails Logger の代わりに簡単に導入できる
- スケーラブル設計(1秒あたりの処理可能なデータサイズはテラバイト)
- スタートガイド
- API リファレンス
- 紹介記事
導入手順
2. GCPプロジェクト上にStackdriver Logging APIをインストールする
3. Gemを追加する
Terminalvi GemfileGemfile# logging gem 'stackdriver-ruby', require: 'stackdriver' gem 'stackdriver'4. Bundle install (gem installでも可)
Terminal$ bundle _1.17.2_ install --path vendor/bundle Fetching gem metadata from https://rubygems.org/......... Fetching gem metadata from https://rubygems.org/. Resolving dependencies... Using rake 13.0.0 …途中省略… Fetching debug_inspector 0.0.3 Installing debug_inspector 0.0.3 with native extensions Fetching binding_of_caller 0.8.0 Installing binding_of_caller 0.8.0 with native extensions …途中省略… Using listen 3.1.5 Fetching multi_json 1.14.1 Installing multi_json 1.14.1 Using mysql2 0.5.2 …途中省略… Fetching google-cloud-env 1.3.0 Installing google-cloud-env 1.3.0 Fetching google-cloud-core 1.4.1 Installing google-cloud-core 1.4.1 Fetching google-protobuf 3.11.2 (universal-darwin) Installing google-protobuf 3.11.2 (universal-darwin) Fetching googleapis-common-protos-types 1.0.4 Installing googleapis-common-protos-types 1.0.4 Fetching grpc 1.25.0 (universal-darwin) Installing grpc 1.25.0 (universal-darwin) Fetching googleapis-common-protos 1.3.9 Installing googleapis-common-protos 1.3.9 Fetching jwt 2.2.1 Installing jwt 2.2.1 Fetching memoist 0.16.2 Installing memoist 0.16.2 …途中省略… Using spring-watcher-listen 2.0.1 Fetching stackdriver-ruby 0.0.1 Installing stackdriver-ruby 0.0.1 …途中省略… Fetching os 1.0.1 Installing os 1.0.1 Fetching signet 0.12.0 Installing signet 0.12.0 Fetching googleauth 0.10.0 Installing googleauth 0.10.0 Fetching rly 0.2.3 Installing rly 0.2.3 Fetching google-gax 1.8.1 Installing google-gax 1.8.1 Fetching stackdriver-core 1.4.0 Installing stackdriver-core 1.4.0 Fetching google-cloud-logging 1.9.1 Installing google-cloud-logging 1.9.1 Fetching google-cloud-debugger 0.35.1 Installing google-cloud-debugger 0.35.1 with native extensions Fetching google-cloud-error_reporting 0.34.0 Installing google-cloud-error_reporting 0.34.0 Fetching google-cloud-trace 0.36.1 Installing google-cloud-trace 0.36.1 …途中省略… Using uglifier 4.2.0 Using web-console 3.7.0 Bundle complete! 31 Gemfile dependencies, 135 gems now installed. Bundled gems are installed into `./vendor/bundle`5. Railsコンフィグに追加する
application.rb# Stackdriver logging settings require "google/cloud/logging" Google::Cloud.configure do |config| # Stackdriver Logging specific parameters config.logging.project_id = "YOUR-PROJECT-ID" config.logging.keyfile = "/path/to/service-account.json" end6. 起動確認
Terminal$ bundle exec rails s -p 8080 Puma starting in single mode... * Version 3.12.1 (ruby 2.6.5-p114), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://localhost:8080 Use Ctrl-C to stop使い方は追って記載します
- 投稿日:2019-12-19T19:54:35+09:00
俺たちは雰囲気でAdminLTEを使っている
はじめに
※この記事は、「RUNTEQ Advent Calendar 2019」、19日目の記事です。
普段、Webサービス・アプリのUIデザインを仕事としてやっているのですが、管理画面の制作は、エンジニアの誰かが良しなに対応してくれているだけという状況がかなり多いです。
社内のRailsエンジニアと話していたときに、「AdminLTEなんかもっといい感じに使えたいんだよね」というお悩み相談を受けたことがあります。
今回は、AdminLTEを使用する際に持っていると助かりそうな知識と、ちょっとしたテクニックをまとめてみました。デザイナーがいないプロジェクトでも、CSSフレームワーク等を用いてせっせと管理画面をつくってくれている、最高のエンジニアの方々のお役に立てたら最高です。
AdminLTEとはなにか
AdminLTEは、Bootstrapベースでつくられた、管理画面作成などに特化したCSSフレームワークです。
これから使うならバージョン3以降がおすすめです。骨組みはBootstrapで、それにスタイリングを加えて、サンプルも提供してくれているものがAdminLTEと考えてもらえると良いかなと思います。
AdminLTE 3の画面サンプルはこちら。
https://adminlte.io/themes/dev/AdminLTE/index.htmlこれはv3.0.0の状態でつくられていて、Bootstrapはv4.3.1、FontAwesomeはv5.11.2が内包されています。
コード一式を手元にダウンロードしたい場合は、GitHubリポジトリからも可能です。
https://github.com/ColorlibHQ/AdminLTE/releases/tag/v3.0.0こんなお悩みありませんか?
いい感じといわれても、ズバッと一言でお答えするのが難しかったので、悩みを集めるところからはじめました。
飲み会や休憩中を狙ってヒアリングした中で出てきたものや、コードをのぞいてて困ってそうだなあと思った事柄はこんな感じでした。お悩み
- いっぱいサンプルがあるけど、どれを使えばいいかわからない
- とりあえずスマホの表示が崩れない状態になりたい
- フッターが欲しいとか途中で言われてかなしい
- ページのタイトルをどうつければいいかわからない
- サイドメニューがなんかいい感じにならない
- テーブルにめっちゃボタンが並んでてやばい
- 入力項目が多すぎてめっちゃ大変に見える
- たまに要素間の余白がなくなったりする
- アラートどこにどんな感じで出したら最高なのかよくわかんない
- 数字がみづらい
- ちょっとだけカスタマイズしたいんだけどどうしよう
個別に答えていくのもできるかもしれませんが、方針やテクニックを知っていれば、解決できそうな予感がします。
あわよくば応用もできるのではと思い、いったんまとめ直してみました。
お答え
- 基本構造を知る
- 便利なclassを使いこなす
- 導線のつくりかたを統一する
- そのページでやれることを絞る
- 項目のグループをつくる
- 上書き用のcssファイルをつくる
解説
順番にみていきます。
基本構造を知る
AdminLTEの構造
AdminLTEのデフォルトサンプルは、ヘッダー、サイドバー、コンテンツ、フッターで構成されています。
body .wrapper nav.main-header aside.main-sidebar .content-wrapper section.content-header .container-fluid section.content .container-fluid # ここらへんにグリッドレイアウトの骨組みが入っていく aside.control-sidebar # サンプルではカスタマイズ用のサイドバー footer.main-footer #sidebar-overlay # スマホサイズでサイドメニュー閉じる用のオーバーレイこの形を崩さなければ、基本構成はほぼ完成です。
Bootstrapのグリッドレイアウト
コンテンツ部分はBootstrapのグリッドレイアウトで組みます。
.rowは入れ子にすることもできます。.container-fluid .row .col-md-6 # 全体の50% .row .col-md-6 # 全体の50%の50%=25% .row .col-md-6 .col-md-6いったん
.col-md-xxだけ使えていれば大丈夫なのですが、横幅によって組み方を変えたい場合はオプションがあったりもします。
- Extra small Small Medium Large Extra large Breakpoint <576px ≥576px ≥768px ≥992px ≥1200px Class .col-xs-xx .col-sm-xx .col-md-xx .col-lg-xx .col-xl-xx 便利なclassを使いこなす
AdminLTE自体にも様々なコンポーネントが用意されています。
ただ、骨組みであるBootstrapには、borderをつけたり余白をつけたりするための便利classが存在して、細かい調整が可能です。例えば、borderだったら
.border-xx。余白だったら.mt-xxや.pr-sm-xxのようなものです。
他には、.alertや.text-rightなんかもよく使います。このあたりを組み合わせると、めっちゃよく使うテーブルのレイアウトを、サンプルよりもうちょっと良い感じにできます。
テーブルレイアウト
Bordered Table
プロジェクトでよくみかけるのは、ボーダーつきのテーブルです。
.card .card-header .card-title .card-tools .card-body table.table.table-bordered thead tbody .card-footerただ、このままだとレスポンシブ対応してません。
そんなときは.card-bodyに.table-responsiveをつけるといい感じになるかと思いきや、上部の線がいなくなってしまいます。.card .card-header .card-title .card-tools .card-body.table-responsive table.table.table-bordered thead tbody .card-footerそんなときは、こうするといい感じです。
.card .card-header .card-title .card-tools .card-body.table-responsive table.table.table-bordered.border-top thead tbody .card-footer良い感じのテーブルができました。
これを鬼のようにコピペすると、管理画面はほぼ完成です。
あとはちょっとしたテクニックですが、数値を表示するときは、小数点以下の桁数を揃えて
.text-rightをつけると見やすくなります。table.table.table-bordered.border-top thead tr th.text-right tbody tr td.text-right導線のつくりかたを統一する
いったんCRUDどおりにやって悩まないのが良いと思います。
サイドメニューに、ユーザー管理という区分を設けて、一覧・作成を配置します。
コンテンツ部分はサンプル通りに、タイトル、パンくず、コンテンツを並べて、このレイアウトを使い回すつもりでやりましょう。
基本の骨組みを省こうとすると、別の調整が必要になるので、いったんは教科書どおりに使うべきです。
そのページでやれることを絞る
一覧ページじゃなくて、詳細ページにだけあればいいものをトルという方針でやるといいです。
一覧ページでは詳細・編集ボタン、詳細ページでは編集・複製・削除ボタンのように区別をするといい感じです。さらに、IDやタイトル名にリンクを貼ると、詳細ボタンを省略できて、アクション部分がスッキリします。
※あくまでも指針なので、複製・削除をすることがものすごく多いのであれば、一覧ページにも置くべきではあります
項目のグループをつくる
例えば、ユーザー新規登録ページがあったとします。単純に項目を並べると、こんな感じになります。
入力項目などが多数ある場合は、似たものを近づけてグループ化してあげると、心理的な負担が和らぎます。
そのグループごとに、見出しをつけるといい感じです。ページ自体は長くなってしまうのですが、小さいゴールがあると、ツラさが少し和らぎます。
ページが長くなるのがどうしてもツラい場合は、レイアウトを変えると、多少短くはできます。
もっと使いやすくするなら、入力エリアは入力量に近い見た目にするといいです。ゴールや負荷が見えたほうが、タスクには取り組みやすくなります。
上書き用のcssファイルをつくる
フォントの指定を変えたり、ちょっとした色味の変更、新しいパーツの追加がしたくなるときがあったりします。
そんなときは、大元のcssをいじるのではなく、新規で上書き用のファイルをつくるといいです。assets内に
_override.scssのようなファイルをつくるのがおすすめです。bodyに
.overrideというclassをつけておくと、セレクタの詳細度があがるのて、上書きがしやすくなる利点もあります。※
.overrideじゃなくて、.customとか.patchとかでもいいかも。body.override ... .container.override { .container { ... } }AdminLTEやBootstrapのドキュメントをみて、使いたいコンポーネントが見つからなかった場合は、最悪自分で書いちゃっていいかなとも思います。HTML&CSSが書ける場合ですが。
その場合、プロジェクトの頭文字などを接頭辞としてつけておくと、class名かぶりを避けやすくなります。
.rt-container { ... }まとめ
というわけで、AdminLTEを使うときに知っていると良さそうな事柄をまとめてみました。
- 基本構造を知る
- 便利なclassを使いこなす
- 導線のつくりかたを統一する
- そのページでやれることを絞る
- 項目のグループをつくる
- 上書き用のcssファイルをつくる
「俺たちは雰囲気でAdminLTEを使っている」とは思いますが、ちょっとした知識を武器にして、もう少しだけでも良い雰囲気で使えるようになってもらえたら最高です。
その他
他にも何か質問がきたり、思いついたりしたら、ここに簡易的なお答えを追記していきます。
- ダッシュボードにはとりあえずサイトマップ的なものを置いておくと便利かも
- layoutファイルにはどこまで記述するか
.content-wrapperまでで、その先は個別のviewファイルに分けるのがいいかなあと思いますおまけ
なぜAdminLTEを使うかの補足
いろいろ理由は並べられるけど、とにかく秒で作り切ることが大事です。
サービスやプロダクトは、管理画面自体をつくりこんでも、ユーザーへの提供価値に直接つなげづらいことが多いです。単純に、エンジニアやデザイナーがじっくりと時間をかけてつくる余裕がなかったりもします。
少ない時間でなんとか最低限の使い勝手も担保したいけれど、バックエンド・サーバーサイドのエンジニアは他にもやることがたくさんあって、CSS、JavaScriptの習得まで手が回りづらいという状況もある気がします。
そんなときにBootstrapやAdminLTEのように、パーツの組み合わせだけでつくりきれるフレームワークが重宝されるのだと思います。
ただ、管理担当者が、素早いオペレーションをできることには十分に価値があるので、いい感じに手軽に管理画面をつくりたい俺たちにとって、AdminLTEのようなものは最高のCSSフレームワークです。
レイアウトの指針
レイアウトの4つの基本原則だけでも知っていると、さらに応用がききそうなので、少しだけふれておきます。
還俗は、近接、整列、反復、対比の4つです。
①近接→関連があるものは近づける、関連がないものは余白をつける
②整列→端をそろえる
③反復→繰り返す
④対比(コントラスト)→違うものには差をつけるきちんと知りたい方は、ノンデザイナーズデザインブックという書籍を読むと最高です。
- 投稿日:2019-12-19T19:00:15+09:00
simple_formのhtmlエスケープを制御する方法
文字列としてviewファイルに書かれないsimpl_formのhtmlエスケープを制御して改行を反映させたい
Rails 5.2.0
ruby 2.5.1やりたかったこと
simple_formのチェックボックス、ラジオボックス、セレクトボックスなどのラベルの表示に、改行を反映させたかった
「文字列としてviewファイルに書かれない」とは、以下のようなものです
<%= f.input :value, as: :check_boxes, collection: recruitment_field_options, label_method: :label, value_method: :code, checked: f.object.check_boxes_value, label: '' %>この場合、チェックボックスのラベルとして表示されるのは、recruitment_field_optionsテーブルのlabelカラムです。
詰まったこと
Rails3以降は、ビューで表示される変数は全てデフォルトでエスケープ処理されます
◆文字列に対してhtmlエスケープを解除する方法は多岐に渡ります
・simple_format(文字列)
・<%== 文字列 %>
・(文字列).html_safeなど
しかし上記のコードの場合、label_methodでカラムを指定しているだけなので、文字列ではありません
◆で挙げた方法ではhtmlエスケープを解除できませんでした解決策
Rubyの無名関数であるlambdaを使用することで解決できました
:labelの部分を以下のコードに置き換えます
lambda { |option| simple_format(option.label).html_safe }全体のコードを以下に示します
<%= f.input :value, as: :check_boxes, collection: recruitment_field_options, label_method: lambda { |option| simple_format(option.label).html_safe }, value_method: :code, checked: f.object.check_boxes_value, label: '' %>lambdaで文字列を取り出しそれぞれにsimple_formatを付けて有効な改行タグ(この場合は<br />)に変換し、html_safeでエスケープを解除していますね
- 投稿日:2019-12-19T19:00:06+09:00
エンティティについて
エンティティ
エンティティとは
- エンティティとは、共通項を持ったデータの集合体
- データの対象を抽象的に捉える。
- 属性とインスタンスを持っている。
- 簡単に言うとER図の箱のこと
- サービスで扱う情報のこと
(例)車をWeb上で販売するアプリ
物理的なエンティティ 社員・店舗・車、顧客など
概念的なエンティティ 購入履歴・進捗状況などエンティティの属性
エンティティの属性とはエンティティが持つ個別の情報のことをいう
車のエンティティの属性は価格、色、大きさ
顧客のエンティティの属性は 名前、年齢、性別memo
- エンティティはDB設計する上で大切な情報。
- DB設計するときは、粒度の大きなエンティティから作成していくといい。
参考
- 投稿日:2019-12-19T16:41:30+09:00
ruby match,gsub正規表現
正規表現入門
match
matchは変数の中に特定の文字があるかどうかを判断してくれる。
name = 'tarou' name.match(/t/) => #<MatchData "t">このように//で囲った文字の有無を確認できる。
//の中の指定方法も様々で
name.match(/[a-z]/)アルファベットのa~zにある文字が入っているのかどうかを確認できる。
ただし小文字のa~zのみの検出になるのでname.macth(/[a-z]/i)とすると大文字、小文字の区別をなくせる。
[0-9]とすれば0~9の数字を検出できます。
webサービスよく使われているものとしては
e_mail_address = 'tarou@gmail.com' e_mail_address.match(/@.+/) => #<MatchData "@gmail.com"> #'.'はどの文字列でも1文字マッチ、'+'は+の前にある文字列に1文字以上続きがあるかを指定。これで入力されたメールアドレスにドメインがついているのかをチェックしたり。
pass = 'TaR0uyamaDa' pass.match(/[a-z\d]{8,16}/i) =>#<MatchData "TaR0uyamaDa"> # [a-z\d]はa~zのうちの一文字、{8,16}で8文字以上16文字以下のように使えばパスワードを登録してもらう時に条件をつけたりできる。
sub,gsub
subは指定した文字を1文字だけ置き換えることができる
str = 'abcaaa' str.sub(/a/, 'x') =>"xbcaaa" #aが1文字だけxに置き換わっている。 lang = '日本語' lang.sub(/日本/,'スワヒリ') =>"スワヒリ語" こんな使い方もできたり。指定した文字を全て置き換えたい場合はgsubを用いる。
phone_num = '123-4567-8910' phone_num.gsub(/-/,"") => "12345678910" #"の中を空にすれば//の中の文字を消すこともできる。電話番号の登録をしてもらう際に'-'があるパターンとそうでないパターン両方でDBに保存すると管理がしずらくなったりするのでそれを解消したりできる。
- 投稿日:2019-12-19T16:36:02+09:00
Rubyのお型付け!Sorbetを試してみた
Ateam cyma Advent Calendar 2019、20日目です!いよいよ終盤ですね!
本日は 株式会社エイチーム のねぼすけエンジニア @NamedPython がお送りします!さて、タイトルで「お?Rubyに型?」と思ったあなた!そう、ついにRubyにも型の時代がやってこようとしているのです!
今日は、RubyKaigi2019にてStripe社から発表があったオープンソースのRuby向け型チェッカーであるSorbet?をふんわり紹介します!どういう経緯・思想で作られたかは、Stripe社のプレゼン: State of Sorbet: A Type Checker for Ruby を見るととても分かりやすいです。ぜんぶ英語ですが聞き取りやすく、自動生成字幕の精度も高いので字幕つけてみるのがおすすめです。
読み方
そるべ って読みます。
カタカナで書くところの「シャーベット」なんですが初見だとわかりませんね。パッと見
どんな感じでアノテーションつけてくのー?っていうサンプルです。公式から引用しています。
Online Playgroundもあるので是非お試しを。
defの真上にあるsigっていうのがアノテーションのDSLになります。sample.rb# typed: true class A extend T::Sig sig {params(x: Integer).returns(String)} def bar(x) x.to_s end end def main A.new.barr(91) # error: Typo! A.new.bar("91") # error: Type mismatch! endこちらがSorbetの型チェッカー(静的解析)の出力
Autocorrect: Use `-a` to autocorrect editor.rb:12: Replace with bar 12 | A.new.barr(91) # error: Typo! ^^^^ editor.rb:13: Expected Integer but found String("91") for argument x https://srb.help/7002 13 | A.new.bar("91") # error: Type mismatch! ^^^^^^^^^^^^^^^ editor.rb:5: Method A#bar has specified x as Integer 5 | sig {params(x: Integer).returns(String)} ^ Got String("91") originating from: editor.rb:13: 13 | A.new.bar("91") # error: Type mismatch! ^^^^ Errors: 2ふむふむ、なんだか目新しい感じですね。
Sorbetの設計思想
僕が感じたことを書くよりも、GithubのREADMEにある公式の設計思想: Sorbet user-facing design principles がいちばんまとまっていたので、拙いながら和訳します:
Sorbetを伴った開発は:
明確であること
型アノテーションによって、コードがより予測可能で読みやすいものになる。
このメリットが明確で、書くことに苦を感じないこと。便利だし、面倒くさくない
メリットが明確なのであれば、(我々Sorbetチームは?)それをより簡潔に実現することにフォーカスする。
これらは以下のようなメリットとして表れる。
- エラーメッセージがわかりやすい
- アノテーションを充実させたぶんだけ、安全性が上がる
最小限だけどパワフル
別に複雑な型システムを作りたいわけじゃない。
必要な分だけ書いて、それ以外はできるだけシンプルに保ちたい。そういうシステムのほうがスケールできるし、(これは一番重要)使う側がSorbetを習得・理解しやすい。Ruby互換
新しい文法はいらない。Rubyコードであれば、ほとんどのツール(エディタ等)がそのまま利用できる。
Sorbetの強みである漸進的改善(gradually improve)を、既存のコードベースに行えるようにする。スケールする
実行速度、開発チームの人数、コード行数、コードの築年数、どんな軸でもスケールする。
僕たちは実際に巨大なRubyコードベースで仕事をしているし、大きくなる一方だから。漸進的に適用できる
スケールする中で(Sorbetを)適用するには、プロジェクトやチームに一気に導入しなければいけない。
つまり、Sorbetは段階的に、プロジェクトに合ったスピードで適用できる必要がある。
ということでした。
使ってみた感想でいうと、
- 簡単に習得できる
- 漸進的に適用できる
が重要なポイントなように思います。
漸進的に適用できるってどゆこと
例えばですが、漸進的ではない適用を考えてみると
- 型アノテーションを付けない限り実行を許さなくなる
- ひたすら型アノテーションがついていない箇所を列挙され、コードベースが大きければ大きいほどエラーメッセージが出続ける
のようなものが考えられますね。
しかしSorbetでは、セットアップの段階で段階的に適用できるようにマジックコメントが用意してあります。# typed:
# typed:は1ファイルごとにつけるものです。
# frozen_string_literal: trueみたいなものですね。以下の5種類があります。
typed: ignoretyped: falsetyped: truetyped: stricttyped: strong解析をしないし、エラーを出さない 解析し、アノテーションがある部分のみエラーを出す 解析し、型を認識できない場合はエラーを出す エラーを出す上に、型アノテーションを強制する すべての型チェックエラーを出す
typed: ignoreとtyped: falseがありますが、この違いが結構大事です。今回はこのふたつのみ詳細を書きます。# typed: ignore
もはやSorbetが解析すらしてくれません。なので、もし
# typed: ignoreなRubyソースを別のソースから参照していると、参照先でエラーが出ることになります。
なので、公式ドキュメントでも可能な限り# typed: ignoreなファイルをなくしていくよう推奨しています。We recommend pushing the entire project to out of ignore (at Stripe, 100% of non-test files are not ignored.)
Stripe社では、テストコード以外はすべて
typed: ignoreではないそうです(すげえ)
# typed: false
- Rubyの構文が正しいか
- 参照が正しいか
sigが書かれているならその構文と実態をチェックする
- 書かれている
sigが矛盾していないかこれが型アノテーションのないファイルのデフォルトの状態です。この状態から少しずつエラーをなくしていく(=アノテーションを付けていく)ようにすれば、漸進的に適用できるってことです。
型チェックのある世界
それでは、型チェックのある世界がどんなものかみてみましょう。セットアップもろもろは省きますが、こんなサンプルコードを用意しました。
sample.rb# typed: true require 'sorbet-runtime' class Sample extend T::Sig FIXED_HASH = { a: 'alpha', b: 'beta' }.freeze sig { params(key: Symbol).returns(String) } def self.typed_nillable_method(key) FIXED_HASH[key] end endなんだか危険なコードですね...
FIXED_HASHにないキーでアクセスするとnilを返すのですが、Sample#typed_nillable_methodで一切制御されていません。でも、
# typed: truesigによるアノテーションができています。
この状態で型チェックをやってみます
# => bundle exec srb tc
おー、型チェック問題なしなんですね。それならこの(危険な)メソッドが外部からアノテーションと違う呼び出され方、返し方をしたとしましょう。
例えばこんなコード:unsafe_code.rb# frozen_string_literal: true # typed: false require_relative 'sample' result = Sample.typed_nillable_method("a") # 文字列を指定 result.split('').join('-')おやおや、
SymbolとアノテーションしているのにStringを渡していますね。この状態で型チェックを行うと....?
すごーーーい!
Expected Symbol but found String("a") for argument key https://srb.help/7002ですって。具体的で親切なメッセージですね。今は手動で実行していますが、
GitLab CIやVSCode Extensionとして仕込むことができれば、コードの安全性担保にもなりますし、生成されたRBIをもとにIDEのメッセージも出せそうです。実際に、その例が Online Playground にはあるのでのぞいてみてください。じゃあ次はこんなコードでどうだ!
unsafe_code.rb# frozen_string_literal: true # typed: false require_relative 'sample' result = Sample.typed_nillable_method(:c) # 存在しないキーを指定 result.split('').join('-')懸念していた危険なケース、存在しないキーを参照しているようです。型チェックは....?
通っちゃいますね...。でも実行すると....?
Return value: Expected type String, got type NilClass (TypeError) Caller: unsafe_code.rb:6 Definition: .../sample.rb:14おお、通常吐かれる
TypeErrorをさらにラップして、該当の部分を詳細に示してくれています。
静的解析もできるし、実行時にもチェックしてくれる....最高か....まとめ
型のあるRuby、
Sorbetいかがでしたでしょうか。情報収集が十分ではないので名前のみにとどめますが、Sorbetの他にも Soutaro Matsumoto さん作の Steep というものもあります。どちらも素敵な思想をもっているので、ぜひ試してみてください。
おわりに
Ateam cyma Advent Calendar 2019、20日目いかがでしたか?
21日目は 早起きエンジニア @shimura_atsushi が 前回 の続きでOCRに挑む 記事を書かれるそうですので、お楽しみに!株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。
エンジニアとしての働き方に興味を持たれた方はcymaのQiita Jobsをご覧ください。
そのほかの職種は、エイチームグループ採用サイトをご覧ください。
- 投稿日:2019-12-19T15:43:05+09:00
fetchmailを用いて自動メール送信作ってみた
アドベントカレンダークリスマスイブ担当koniaです。
自己紹介
この会社に入り本格的にエンジニアとして働き出して早1年目、まだまだ足りないもの感じながらしごかれて最後には星になります。そんな若輩者の拙い文章ですが、ちょっとした時間に読んでいただき1つでも為になったら心中でワルツが踊り出します。
ふとした依頼
さて、自己紹介はこれくらいにして最近サークルの後輩からこういう依頼が来ました。
「最近イベントとかするんですけど、そのメールを自動で返信してくれるシステム的なの欲しいんですよ…」
「サクッと作れないですかね」
お、これは作るしかないと。いろんなサービスが今や横行していますが、なんかちょっとエンジニアチックに作りたいなと思って色々みていました。
そこで見つけたのが、fetchmailというOSSです。リモートのメールサーバからメールを取得し、 これをローカル (クライアント) マシンの配送システムに転送します。 受け取ったメールは普通のメールユーザエージェントで扱うことができます。
https://linuxjm.osdn.jp/html/fetchmail/man1/fetchmail.1.html
こちらを参照と言ったようにメールを扱うOSSがありましたのでこちらを使用していきます。
自動メール送信
OSS : fetchmail
OS : Ubuntu 18.04
言語 : Ruby
OS、言語選定については後輩がわかるものに揃えた為、特に理由があったわけではないので他でもできます。fetchmail設定ファイルについて
#86400秒(24時間)ごとに起動する set daemon 86400 defaults uidl no mimedecode no rewrite # メールを渡すプログラムのフルパス mda /home/name/process/textProcessing.rb keep #メールサーバの情報 poll mail.example.jp protocol POP3 username example@example.jp password xxxxxxxx ssl特定の時間がきた場合、textProcessing.rbが起動するようになってます。
次にtextProcessing.rbのコードを示します。
このコード内部で行なっているのはイベント登録に来たメールからデータを取得し、CSV吐き出しにしています。(今回はDBを用いていません。)#! /usr/bin/ruby # coding: utf-8 #------------------------------------------------- # Ruby script to get a mail via alias of postfix. #------------------------------------------------- require 'mail' require 'csv' class GetMail def initialize #csvの格納場所 @out_csv = "/home/konia/deskTop/mail.csv" end # イベント登録した際の学生情報メールを改行とコロンによって情報を配列化 def execute CSV.open(@out_csv, "a") do |f| mail = Mail.new($stdin.read.gsub(/\r?\n/,"\r\n")) mail_sender = mail.from.first mail_subject = mail.subject.to_s mail_body = mail.body.to_s mail_body = mail_body.force_encoding("utf-8") mail_array = mail_body.scan(/:\s*([^\r]*)/) if mail_array.length >= 7 && mail_sender == "hoge@gmail.com" && mail_subject.scan("event").empty? date = mail_array[6].to_s.scan(/(\d+[^\(])/) time = date[3] date.slice!(3,2) mail_array[6] = date.join mail_array[7] = time # 配列化した情報をcsvで取り込む f << mail_array.flatten end end rescue => e $stderr.puts "[#{e.class}] #{e.message}" e.backtrace.each{|trace| $stderr.puts "\t#{trace}"} exit 1 end end exit unless $0 == __FILE__ GetMail.new.executeそこから次はメールを送信する際の処理を下記に示します。
CSVから特定のデータを取得し、それを整形し特定の日付になった際にメール送信を行う。#! /usr/bin/ruby # coding: utf-8 #------------------------------------------------- # Ruby script to get a mail via alias of postfix. #------------------------------------------------- require 'mail' require 'csv' require 'time' class TextProcess def initialize @@from_mail = '送信者' @@to_mail = '受信者' @csv = "/home/hashi/deskTop/mail.csv" end # csvファイルから時間帯のデータを取ってき、イベント3日前と前日についての学生をまとめる def mailProcess student_info = {} flg_3 = i = 0 CSV.foreach(@csv) do |cs| date = Time.strptime(cs[6].to_s,'%Y年%m月%d日') today = Time.now three_date_before = date - 259200 two_date_before = date - 172800 one_date_before = date - 86400 if three_date_before <= today && today < two_date_before flg_3 = 1 student_info[i] = cs i += 1 elsif one_date_before <= today && today < date sendMail(2,cs) end end if flg_3 == 1 processed_text = textProcess(student_info) sendMail(1,processed_text) end end # 上記のイベント前に合致する学生情報をメールの本文に作成する def textProcess(tmp) i = 0 info = "" for j in 0...tmp.length do info << "氏名:#{tmp[j][0].to_s}\nメールアドレス:#{tmp[j][4].to_s}\n会場:#{tmp[j][5].to_s}\n日時:#{tmp[j][6].to_s+tmp[j][7].to_s}\n\n\n" i += 1 end return info end # 作成したメールについて送信を行う def sendMail(flg,tmp) if flg == 2 mail = Mail.new do from @@from_mail to @@to_mail cc @@to_mail2 bcc @@to_mail3 subject "前日のお知らせです" body "氏名:#{tmp[0].to_s}\nメールアドレス:#{tmp[4].to_s}\n会場:#{tmp[5].to_s}\n日時:#{tmp[6].to_s+tmp[7].to_s}" end elsif flg == 1 mail = Mail.new do from @@from_mail to @@to_mail cc @@to_mail2 bcc @@to_mail3 subject "3日前のお知らせです" body tmp end end mail.charset = 'utf-8' mail.delivery_method(:smtp, address: "送信サーバ", port: ポート番号, domain: "ドメイン名", authentication: :login, user_name: "メールアドレス", password: "送信サーバのパスワード", enable_starttls_auto: false ) mail.deliver end end exit unless $0 == __FILE__ TextProcess.new.mailProcess以上となります。OSS使って書いてみると色々と気づくものもありますが今の時代なんでもそろってるからそっちを使った方が楽なのは間違いないですね?
- 投稿日:2019-12-19T15:36:28+09:00
○witterもどきのアプリでハッシュタグ機能を実装してみた。
はじめに
どうも、Pirikaraです。
今回は適当に作った○witterもどきのアプリで、ハッシュタグ機能を実装してみたので共有したいと思います。まずはrails new
まずはT○itterもどきのアプリを作ります。
tag機能を実装するので、rails newでtag_appを作ります(適当)tweetモデルとtagモデルを作成しますが、
両者は多対多の関係なので、中間テーブルとしてtweet_tagモデルも作っておきます。tweet.rbclass Tweet < ApplicationRecord has_many :tweet_tags has_many :tags, through: :tweet_tags #写真の投稿も可能にするために、carrierwaveとminimagickを入れておいた mount_uploader :image, ImageUploader endtag.rbclass Tag < ApplicationRecord has_many :tweet_tags has_many :tweets, through: :tweet_tags endtweet_tag.rbclass TweetTag < ApplicationRecord belongs_to :tweet belongs_to :tag endまずはImageとTextのみ投稿ができるTw○tterもどきアプリを作りました。
tweets_controller.rbclass TweetsController < ApplicationController def index @tweets = Tweet.all end def new @tweet = Tweet.new end def create tweet = Tweet.create(tweet_params) redirect_to tweets_path end private def tweet_params params.require(:tweet).permit(:image, :text, :tag_ids) end end現在『Tags』のプルダウンには何も入っていませんが、Tagsテーブルに登録されているものが表示される仕様です。
今回はtextarea内に『#(ハッシュタグ)』があれば、それ以下をtagとして認識、Tagsテーブルに保存される形でコードを書いていきたいと思います。仕様と進め方の確認
- textareaに『#(ハッシュタグ)』があれば『#』以下をtagとして認識し、Tagsテーブルに保存する
- 一覧表示画面にて、tag名が『#○○○』の形で表示される
- 新規投稿画面にて、Tagsのプルダウンにこれまで登録されたTagが表示される。
今回はtweets_controllerのcreateアクションでTweet.createされたあと、
model/tweet.rbでActiveRecordの『after_create』というコールバックを使用し、データベースに保存される直前にTagsテーブルに『#』タグ以下を登録させる感じでやっていきます。
after_create等のコールバックについては、下記のサイトを参考にしました。それではいざ、実装。
Tweet Modelにコールバックの記述
after_create do 〜 endの間に処理を記述していきます。
1. controller側でcreateしたTweetを取得
2. Tweetのtextカラムの値から、『#〇〇〇』に該当する箇所を検出
3. tagsテーブルに保存加えて、view側で『#〇〇〇』をtagsテーブル経由で表示させまます。
4.『#〇〇〇』を含む形でtweetsテーブルに保存されているので、viewで出力する際に正規表現を使って取り除く。
コードは以下の通りです。
models/tweet.rbclass Tweet < ApplicationRecord has_many :tweet_tags has_many :tags, through: :tweet_tags mount_uploader :image, ImageUploader #DBへのコミット直前に実行 after_create do #1.controller側でcreateしたTweetを取得 tweet = Tweet.find_by(id: self.id) #2.正規表現を用いて、Tweetのtext内から『#○○○』の文字列を検出 tags = self.text.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/) #3.mapメソッドでtags配列の要素一つ一つを取り出して、先頭の#を取り除いてDBへ保存する tags.uniq.map do |t| tag = Tag.find_or_create_by(name: t.downcase.delete('#')) tweet.tags << tag end end endtweets/index.html.erb<div class="content"> <% @tweets.each do |tweet| %> <div class="content__post"> <%=image_tag "#{tweet.image}"%> <div class="content__post__tags"> <!--4.gsub()で正規表現に合致する箇所を空欄にする--> <%= simple_format(tweet.text.gsub!(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/, "")) %> <!--4.tweetにタグがあればタグを出力する--> <% if tweet.tags.any? %> <% tweet.tags.each do |tag| %> <p>#<%= tag.name %></p> <% end %> <% end %> </div> </div> <% end %> </div> <div class="footer"> </div>index view側でハッシュタグとともにtweetを表示させることができました。
DBにもちゃんと保存されていますね。
めでたしめでたし。
おわりに
今回はTwitte○もどきのアプリでハッシュタグ機能の実装にチャレンジしてみました。
View側で表示させる時に正規表現を使っているところが不恰好なのでヘルパーに突っ込んでもいいかもしれませんね。応用としては、ハッシュタグからリンクを飛ばして紐づいたtweetを一覧表示できる・・・・・・とかですかね。
ぜひチャレンジしてみてください。おわり。
- 投稿日:2019-12-19T15:28:06+09:00
Protocol Buffers を使って、Rails の RESTful API をスキーマ駆動開発してみた
こんにちは、クラウドワークスの @t0yohei です。普段は Rails や Vue.js などを使って crowdworks.jp の開発をしています。
この記事は クラウドワークス Advent Calendar 2019 の20日目の記事です。昨日は @juntetsu_tei による、Rails更新は最低限Controllerのテストが欲しいというお話でした。
今回は、Protocol Buffers を使った、 Rails の RESTful API はこんな感じに開発できるんじゃない?って内容です。実務で実際に使ったわけではないので、不足している点があると思いますが悪しからず。
用語の整理
Protocol Buffers って?
Google が社内向けに開発を始めたツールです。2008年以降 OSS として公開されており、誰でも閲覧、 contribute することができます。https://github.com/protocolbuffers/protobuf
Protocol Buffers は protobuf と略称して呼ばれており、この記事でも今後 protobuf という呼び方を基本的に使いたいと思います。
GitHub の Overview によると、
Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data.
Protocol Buffers(別名、protobuf)は、構造化データをシリアライズするための Google の言語中立、プラットフォーム中立な拡張可能なメカニズムです。(著者訳)
と書かれていますが、正直難しい。個人的な理解としては、「プロセス間(システム間)でデータをやりとりする際のシリアライズ形式と、シリアライズに必要な各種ライブラリ及びスキーマ定義のためのDSL」という認識です(わかりにくい)。
Protocol Buffers 美しい!一番好きなスキーマ定義言語です!
先ほどの説明の最後に、
スキーマ定義のためのDSLというワードが出てきましたが、この DSL がとても美しく、今でも protobuf が生き残っている由縁と言われています。
サンプルとして、公式ガイドに載っているスキーマ定義を載せておこうと思います。 protobuf のことを知らなくても、なんとなくどんな内容が書かれているのか想像することができるかと思います。message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; }スキーマ定義言語とは何か、なぜそれが重要かといったことは、今さらProtocol Buffersと、手に馴染む道具の話という記事に全てまとめられているので、こちらをご参照いただければと思います。
自分のようにリンク先を読むのが面倒だという人のために、スキーマ定義言語とは何かということについて、自分なりの説明を試みてみようと思います。
スキーマと聞くと、多くの方はDBスキーマを想像するかと思いますが、スキーマ本来の意味としてはDBに限らない「構造」となります。 protobuf で定義するものは、この「構造」の中でも、プロセス間(システム間)でやりとりされるデータの構造(形式・内容)となります。
protobuf と同種のスキーマを定義する言語としては、他にも Open API(旧Swagger) や GraphQL が上げられるかと思います(GraphQLについては、GraphQLのDSLと読み替えた方が正しいかもしれません)。 (注釈1)protobuf は gRPC のスキーマ定義の標準言語に位置付けられており、 gRPC の発達とともに最近再度注目を集めている印象です。
protobuf の詳細について知りたい方は、 Google の protobuf に関する公式ガイドをお読みください。
自分のように、なんやかんや日本語訳が読みやすいよねって方は、上記を翻訳してみたこちらの記事をご参照ください。スキーマ駆動開発
プロセス間(システム間)でやりとりされるデータの構造(スキーマ)を一番最初に決めて、開発を進める手法です。テスト駆動開発がテストを書くことから始まり、テストに駆動された実装になるように、スキーマ駆動開発でもスキーマを先に定義して、スキーマに駆動される開発を行います。
protobuf では、定義したスキーマから実装に使用するコードを自動生成します。この点から、開発がスキーマに駆動されている感覚があり、またスキーマから自動生成されたコードを元に実装を組み上げるため、スキーマ定義と実装間の完全性が保たれます。
オレオレスキーマ定義(ドキュメントにプロセス間でやりとりされるデータをいい感じにまとめる手法)や、 Open API などを使ったスキーマ定義との違いが現れるのがこの部分で、実装とドキュメントが解離しているといった不幸をなくすことができます。 protobuf 素晴らしい。
実装イメージ
前置きが長くなりましたが、 protobuf を使った、Rails の RESTful API がどんな感じになるのかを書いていきます。今回は雰囲気を掴んでもらうことを主眼に置いているので、チュートリアル形式にはなっていません。実際に実装を試したいと思った方は、付録1に簡単なチュートリアルっぽいものを用意しているのでご参照ください。
proto ファイルでのスキーマ定義
まずは proto ファイルでのスキーマ定義です。 今回は、フロントエンドをSPAで実装することを想定した TODO リストにおける Rails の RESTful API のうち、 index, show, create の各アクションについて定義していきます。各アクションではそれぞれ、 Request/Response の形式・内容を定義しています。
task.protosyntax = "proto3"; option ruby_package = "Protos::"; /* GET proto/tasks タスク一覧の取得 */ // message FetchTasksRequest {} message FetchTasksResponse { Tasks tasks = 1; } /* GET proto/task/:id タスク一件の取得 */ // message FetchTaskRequest {} message FetchTaskResponse { Task task = 1; } /* POST proto/tasks タスクの新規追加 */ message CreateTaskRequest { string title = 1; string description = 2; } /* タスクの新規追加成功・失敗時のレスポンス Scuccess Status: 201 Error Status: 400... */ message CreateTaskResponse { Status status = 1; } message Tasks { repeated Task task = 1; } message Task { int32 id = 1; string title = 2; string description = 3; } message Status { int32 code = 1; string message = 2; }細かく見ていきます。まずは
GET proto/tasks タスク一覧の取得の部分。task.proto/* GET proto/tasks タスク一覧の取得 */ // message FetchTasksRequest {} message FetchTasksResponse { Tasks tasks = 1; } message Tasks { repeated Task task = 1; } message Task { int32 id = 1; string title = 2; string description = 3; }この部分では、タスク一覧取得の index アクションについて定義しています。
リクエスト時は、エンドポイントをそのまま叩くだけなので、データの内容を定義しません(定義しないことを明確にするためにコメントアウトしています)。
レスポンスとして、 id, title, description を持つ、 Task 型のデータを複数持つ Tasks 型の値を返します(ややこしい)。次に、
GET proto/task/:id タスク一件の取得の部分。task.proto/* GET proto/task/:id タスク一件の取得 */ // message FetchTaskRequest {} message FetchTaskResponse { Task task = 1; }指定した id を持つタスク一件を取得する show アクションに対する定義です。
リクエスト時は、 id に対するエンドポイントをそのまま叩くだけなので、データの内容を定義しません。 (注釈2)
レスポンスとして、 id, title, description を持つ、 Task 型の値を返します。最後に、
POST proto/tasks タスクの新規追加の部分。task.proto/* POST proto/tasks タスクの新規追加 */ message CreateTaskRequest { string title = 1; string description = 2; } /* タスクの新規追加成功・失敗時のレスポンス Scuccess Status: 201 Error Status: 400... */ message CreateTaskResponse { Status status = 1; } message Status { int32 code = 1; string message = 2; }タスクの新規追加の create アクションの定義です。
リクエストのデータとして、 title と description を送るように定義します。
また、レスポンスとしては、成功・失敗などのステータスを返却するように定義します。スキーマ定義を元に、実装で使用するコードを自動生成
上記で作成した proto ファイルの定義を元に、実際の実装で使用するコードを自動生成します。
$ protoc --ruby_out=./lib proto/task.proto自動生成される内容がこちら。
lib/proto/task_pb.rb# Generated by the protocol buffer compiler. DO NOT EDIT! # source: proto/task.proto require 'google/protobuf' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("proto/task.proto", :syntax => :proto3) do add_message "FetchTasksResponse" do optional :tasks, :message, 1, "Tasks" end add_message "FetchTaskResponse" do optional :task, :message, 1, "Task" end add_message "CreateTaskRequest" do optional :title, :string, 1 optional :description, :string, 2 end add_message "CreateTaskResponse" do optional :status, :message, 1, "Status" end add_message "Tasks" do repeated :task, :message, 1, "Task" end add_message "Task" do optional :id, :int32, 1 optional :title, :string, 2 optional :description, :string, 3 end add_message "Status" do optional :code, :int32, 1 optional :message, :string, 2 end end end module Protos FetchTasksResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTasksResponse").msgclass FetchTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTaskResponse").msgclass CreateTaskRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskRequest").msgclass CreateTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskResponse").msgclass Tasks = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Tasks").msgclass Task = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Task").msgclass Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Status").msgclass endクラス定義わかりにくい...って思うかもしれませんが、その話はここではしない。
自動生成されたコードを使用した各アクションの実装
次に、自動生成されたコードを活用してコントローラーを実装していきます。
app/controllers/proto/tasks_controller.rbclass Proto::TasksController < ApplicationController def index tasks = Task.all tasks_proto = Protos::Tasks.new tasks.each do |each_task| task_proto = Protos::Task.new( id: each_task.id, title: each_task.title, description: each_task.description ) tasks_proto.task.push(task_proto) end response = Protos::FetchTasksResponse.new(tasks: tasks_proto) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data end def show task = Task.find(params[:id]) task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) response = Protos::FetchTaskResponse.new(task: task_proto) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data end def create decoded_data = Protos::CreateTaskRequest.decode(request.raw_post) task = Task.new(title: decoded_data.title, description: decoded_data.description) if task.save status = Protos::Status.new(code: 201, message: "#{task.title}を作成しました。") render plain: build_create_message_encoded(status: status) else status = Protos::Status.new(code: 400, message: "#{task.title}の作成に失敗しました。") render plain: build_create_message_encoded(status: status) end end private def build_create_message_encoded(status:) message = Protos::CreateTaskResponse.new(status: status) Protos::CreateTaskResponse.encode(message) end endこちらも細かく見ていきます。
show
順番が少し前後しますが、まずは簡単な show アクションから。
app/controllers/proto/tasks_controller.rbdef show task = Task.find(params[:id]) task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) response = Protos::FetchTaskResponse.new(task: task_proto) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data end指定された id に合致するタスクを一件取得して、 自動生成で作成された
Protos::Taskクラスのインスタンスオブジェクトにタスクのデータを詰め替えます。
詰め替え後のデータを元に、レスポンスオブジェクトを作成し、encodeメソッドでレスポンスオブジェクトをシリアライズします。そして最後に、シリアライズされたデータを返却します。
せっかくなので、各部分でデータがどう変化していくかをコメントに書いてみました。task = Task.create(id: 1, title: 'title', description: 'description') # => #<Task id: 1, title: "title", created_at: "2019-12-17 08:30:09", updated_at: "2019-12-17 08:30:09", description: "description"> task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) # => <Protos::Task: id: 1, title: "title", description: "description"> response = Protos::FetchTaskResponse.new(task: task_proto) # => <Protos::FetchTaskResponse: task: <Protos::Task: id: 1, title: "title", description: "description">> response_encoded_data = Protos::FetchTaskResponse.encode(response) # => "\n\x16\b\x01\x12\x05title\x1A\vdescription"最終的には、
"\n\x16\b\x01\x12\x05title\x1A\vdescription"のような文字列がレスポンスとして返されることになります。
このデータの呼び方は特に定義されていないのですが、 (protobuf の)シリアライズデータと呼ぶことにします。
このシリアライズデータには、型情報や実際の値が含まれているので、同じ proto ファイルのスキーマ定義を元に自動生成されたクラス定義(多言語でも可)を使用することで、受け取り先で decode することができます。もちろん、 encode したシリアライズデータをそのまま decode することもできます(実際の開発でやることは少ないと思いますが)。
response_encoded_data = Protos::FetchTaskResponse.encode(response) decoded_data = Protos::FetchTaskResponse.decode(response_encoded_data) # =><Protos::FetchTaskResponse: task: <Protos::Task: id: 1, title: "title", description: "description">>また、それでも json が好きだ!という場合は、 json に変換することも可能です。
response = Protos::FetchTaskResponse.new(task: task_proto) Protos::FetchTaskResponse.encode_json(response) # => "{\"task\":{\"id\":1,\"title\":\"title\",\"description\":\"description\"}}"index
次に index アクションについて見ていきます。
app/controllers/proto/tasks_controller.rbdef index tasks = Task.all tasks_proto = Protos::Tasks.new tasks.each do |each_task| task_proto = Protos::Task.new( id: each_task.id, title: each_task.title, description: each_task.description ) tasks_proto.task.push(task_proto) end response = Protos::FetchTasksResponse.new(tasks: tasks_proto) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data endshow アクションとの違いは特にないのですが、データの詰め替えの部分の処理が長ったらしくなってしまっています... この辺は要リファクタリングです。
create
最後に、 create アクションです。
app/controllers/proto/tasks_controller.rbdef create decoded_data = Protos::CreateTaskRequest.decode(request.raw_post) task = Task.new(title: decoded_data.title, description: decoded_data.description) if task.save status = Protos::Status.new(code: 201, message: "#{task.title}を作成しました。") render plain: build_create_message_encoded(status: status) else status = Protos::Status.new(code: 400, message: "#{task.title}の作成に失敗しました。") render plain: build_create_message_encoded(status: status) end end private def build_create_message_encoded(status:) message = Protos::CreateTaskResponse.new(status: status) Protos::CreateTaskResponse.encode(message) end
request.raw_postで post で送られてくる(protobuf の)シリアライズデータを取得して、デコードします。デコード後は通常の create 処理と同様に save してあげて、レスポンスデータを show メソッドなどと同様の方法で作成し、レスポンスを返してあげます。まとめ
ひとまず、 proto ファイルでスキーマを定義してからレスポンスを返す実装を一通り紹介できたかなと思います。最後に、 protobuf を使ったスキーマ駆動開発って何がいいの?または何が悪いの?って点について個人的な感想を書きたいと思います。
良い点
- 簡素で明瞭なスキーマ定義が書ける。
- 自動生成するコードを元に実装を進めるので、スキーマ定義と実装が基本的に一致する。
- スキーマを先に決めて開発するので、フロントエンド・バックエンドなどの実装で認識の齟齬が起きにくい。
- スキーマ定義とそれを元に自動生成されるコードがあるので、バックエンドの実装を待たずに、フロントエンドでモックを作ったりテストを書いたりできる。
悪い点
- 自動生成されるコードに依存する実装になるので、やめたい時に引き剥がずのが一手間かかる。
- protobuf のドキュメントや実装サンプルが豊富ではないので、学習がちょっとやりにくい。
実際に業務に適用できるかどうかは、チームや組織の状況によりそうです。個人的には JSON や YAML を使ったスキーマ定義よりも protobuf の方が見やすいと思うので、スキーマ駆動開発をする際は是非とも使いたいという気持ちです。
ひとまず本編は以上になります。最後まで読んでいただきありがとうございました。
付録1 チュートリアルのようなもの
この付録1では、実際に protobuf を使った rails の RESTful API を作成していく手順について書いていこうと思います。
本編と同様に、フロントエンドをSPAで実装することを想定した TODO リストを想定して開発を進めていきます。railsの基本的な環境構築は省略しますが、自分は下記の手順で構築しましたので必要に応じて参考にしてください。
https://qiita.com/t0yohei/items/9f3a418d3ea61e090be3また、実装全体やテスト、 Nuxt.js の SPA への適用法などの詳細が知りたい場合は、下記のリポジトリを参照いただければと思います。
https://github.com/t0yohei/rails-nuxt-protobuf-todoでは、早速進めていきましょう。
環境構築
今回は下記の環境で動作することを確認しています。
ruby: 2.6.4 rails: 6.0.0protoc のインストール
proto ファイルから実装コードを自動生成する際に、
protocというライブラリが必要になるのでインストールします。$ brew install protobufprotobuf 用の gem のインストール
今回は、 google に開発が主導されている google-protobuf という gem を使用します。
gem の選定に関しては、付録2 gem の選定の章で話そうと思います。Gemfile に
gem 'google-protobuf'を追記して、bundle installを実行してください。model, migration ファイルの作成
まずは model, migration ファイルを作成して、タイトルと詳細だけを保持するシンプルなタスクテーブルを作成します。
$ bundle exec rails g model Task title:string description:stringvalidation の追加
app/models/task.rbclass Task < ApplicationRecord validates :title, presence: true, length: { in: 1..25 } validates :description, length: { maximum: 100 } end各種制約の追加
db/migrate/20190000000000_create_tasks.rbclass CreateTasks < ActiveRecord::Migration[6.0] def change create_table :tasks do |t| t.string :title, :string, null: false, default: '', limit: 25 t.string :description, :string, limit: 100 t.timestamps end end endmigration を実行しましょう。
$ bundle exec rails db:migrate続いて、開発がしやすいように seed データを準備します。
db/seeds.rbTask.create(title: '大掃除', description: '年末なので気だるいけど大掃除をする') Task.create(title: 'advent calendar を書く', description: 'そろそろ書き始めないとヤバイ')忘れずデータを投入しておきましょう。
$ bundle exec rails db:seedproto ファイルでの スキーマの定義
次に、システム間でどういうデータ形式でデータをやりとりするのかを、 proto ファイルを使用して定義します。
今回は RESTful な API なので、 Request/Response をそれぞれ定義しています。proto/task.proto/* command: protoc --ruby_out=./lib proto/task.proto */ syntax = "proto3"; option ruby_package = "Protos::"; /* GET proto/tasks タスク一覧の取得 */ // message FetchTasksRequest {} message FetchTasksResponse { Tasks tasks = 1; } /* GET proto/task/:id タスク一件の取得 */ // message FetchTaskRequest {} message FetchTaskResponse { Task task = 1; } /* POST proto/tasks タスクの新規追加 */ message CreateTaskRequest { string title = 1; string description = 2; } /* タスクの新規追加成功・失敗時のレスポンス Scuccess Status: 201 Error Status: 400... */ message CreateTaskResponse { Status status = 1; } message Tasks { repeated Task task = 1; } message Task { int32 id = 1; string title = 2; string description = 3; } message Status { int32 code = 1; string message = 2; }
option ruby_package = "Protos::";の部分で、自動生成するクラスのモジュールを定義しています。こうしておくことで、 ActiveRecord の Task クラスとの名前衝突を回避することができます。続いて、実装で使用するコードの自動生成します。
$ protoc --ruby_out=./lib proto/task.proto下記のようなコードが生成されているかと思います。
lib/proto/task_pb.rb# Generated by the protocol buffer compiler. DO NOT EDIT! # source: proto/task.proto require 'google/protobuf' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("proto/task.proto", :syntax => :proto3) do add_message "FetchTasksResponse" do optional :tasks, :message, 1, "Tasks" end add_message "FetchTaskResponse" do optional :task, :message, 1, "Task" end add_message "CreateTaskRequest" do optional :title, :string, 1 optional :description, :string, 2 end add_message "CreateTaskResponse" do optional :status, :message, 1, "Status" end add_message "Tasks" do repeated :task, :message, 1, "Task" end add_message "Task" do optional :id, :int32, 1 optional :title, :string, 2 optional :description, :string, 3 end add_message "Status" do optional :code, :int32, 1 optional :message, :string, 2 end end end module Protos FetchTasksResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTasksResponse").msgclass FetchTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTaskResponse").msgclass CreateTaskRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskRequest").msgclass CreateTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskResponse").msgclass Tasks = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Tasks").msgclass Task = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Task").msgclass Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Status").msgclass endlib 配下に 自動生成されるファイルを置くようにしたので、下記を追記してちゃんと読み込まれるようにします。
config/application.rbDir["#{Rails.root}/lib/proto/*.rb"].each { |file| require file }route.rb で API のエンドポイントを定義
route.rbRails.application.routes.draw do namespace :proto do resources :tasks end endcontroller の実装
app/controllers/proto/tasks_controller.rbclass Proto::TasksController < ApplicationController def index tasks = Task.all tasks_proto = Protos::Tasks.new tasks.each do |each_task| task_proto = Protos::Task.new( id: each_task.id, title: each_task.title, description: each_task.description ) tasks_proto.task.push(task_proto) end response = Protos::FetchTasksResponse.new(tasks: tasks_proto) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data end def show task = Task.find(params[:id]) task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) response = Protos::FetchTaskResponse.new(task: task_proto) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data end def create decoded_data = Protos::CreateTaskRequest.decode(request.raw_post) task = Task.new(title: decoded_data.title, description: decoded_data.description) if task.save status = Protos::Status.new(code: 201, message: "#{task.title}を作成しました。") render plain: build_create_message_encoded(status: status) else status = Protos::Status.new(code: 400, message: "#{task.title}の作成に失敗しました。") render plain: build_create_message_encoded(status: status) end end private def build_create_message_encoded(status:) message = Protos::CreateTaskResponse.new(status: status) Protos::CreateTaskResponse.encode(message) end endちゃんと動くか動作確認をします。
$ bundle exec rails s
http://localhost:3000/proto/tasksを見てみると、文字化けしたものが出力されていると思います。(自分の環境ではゴミデータも混ざっているので、写真の通りには出てこないと思いますが悪しからず)テストの作成
長くなりそうだったので、 index アクションのテストだけを記載しました。他のアクションのテストについては、こちらのリポジトリをご確認ください。
spec/requests/proto/tasks_spec.rbrequire 'rails_helper' RSpec.describe "Proto::Tasks", type: :request do describe "GET /proto/tasks" do subject { get proto_tasks_path } it "特に条件を指定しなくても、200を返すこと" do subject expect(response).to have_http_status(200) end context "taskが1件も存在しないとき" do it "空の encoded_data が返却されること" do subject expect(response).to have_http_status(200) decoded_response = Protos::FetchTasksResponse.decode(response.body) expect(decoded_response.tasks.task).to be_empty end end context "taskが1件存在するとき" do let!(:task) { Task.create(id: 1, title: 'title', description: 'description') } it "task1件分の encoded_data が返却されること" do subject decoded_response = Protos::FetchTasksResponse.decode(response.body) expect(decoded_response.tasks.task.count).to eq(1) expect(decoded_response.tasks.task.first.id).to eq(task.id) expect(decoded_response.tasks.task.first.title).to eq(task.title) expect(decoded_response.tasks.task.first.description).to eq(task.description) end end context "taskが2件存在するとき" do let!(:task1) { Task.create(id: 1, title: 'title1', description: 'description1') } let!(:task2) { Task.create(id: 2, title: 'title2', description: 'description2') } it "task2件分の encoded_data が返却されること" do subject decoded_response = Protos::FetchTasksResponse.decode(response.body) expect(decoded_response.tasks.task.count).to eq(2) expect(decoded_response.tasks.task.first.id).to eq(task1.id) expect(decoded_response.tasks.task.first.title).to eq(task1.title) expect(decoded_response.tasks.task.first.description).to eq(task1.description) end end end endindex, show メソッドをリファクタリング
index アクションにコードが長々とあるのでリファクタリングをします。
app/controllers/proto/tasks_controller.rbdef index response = Protos::FetchTasksResponse.new(tasks: Task.convert_all_to_message_object) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data, status: :ok end def show task = Task.find(params[:id]) response = Protos::FetchTaskResponse.new(task: task.convert_to_message_object) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data, status: :ok endapp/models/task.rbclass Task < ApplicationRecord def self.convert_all_to_message_object Protos::Tasks.new( task: Task.all.map { |task| task.convert_to_message_object } ) end def convert_to_message_object Protos::Task.new( id: self.id, title: self.title, description: self.description ) end endリファクタリングについては、こちらの記事を参考にさせてもらいました。
Building APIs with Rails + Protocol Buffersこれで、一通りの実装が作成できたかと思います。お疲れ様でした。
update, destroy アクションの実装が気になる場合は、こちらをご覧ください。
https://github.com/t0yohei/rails-nuxt-protobuf-todo/blob/master/backend/app/controllers/proto/tasks_controller.rb付録2 gem の選定
今回の実装では、 google が公式に出している、 google-protobuf を使用しました。
ruby で protobuf を使用する際に導入できそうな gem としては他にも、 protobuf などがあります。(ややこしいですがgoogleの公式の方に
後者の gem も今でもメンテナンスされており、使用しても問題ないように思います。
(なぜ複数の active な gem が存在しているのかということはちゃんとは調べていないのですが、公式の protobuf の ruby ライブラリ(gem)より先に、有志の gem が作られたのかなー?と勝手に予想しています。現に proto2 の時点では ruby で protobuf を扱うことができなかったようです(現在は proto3 がデフォルトなので1つ前のバージョンの時)。)どちらの gem を選択するかは難しいところですが、自分が知っている限りでのそれぞれの特徴を書いておこうと思います。
google-protobuf
リポジトリ: google-protobuf
- google が公開しているものなので、ちゃんとメンテナンスされ続けそう。
- gem の実装に Java とか C などのコードが使われている。
- 自動生成される ruby のコードがわかりにくい。
protobuf
リポジトリ: protobuf
- メンテナンスは今でもされていそう(2019年12月時点)。
- ruby で実装されている(重要)。
- 自動生成される ruby のコードがシンプルでわかりやすい。
- protobuf-activerecord を使う際に必須になる。
まとめ
個人で趣味として使うのであれば protobuf の方が、使いやすそうだなーと個人的に思っています。
protobuf-activerecordを使って、実装をシンプルにできそうだし。業務で使う場合にどちらを使用するかは、皆さんでご判断ください。付録3 例外処理
protobuf で自動生成されたコードを使って実装を進める場合、レスポンスは proto ファイルで定義した response の型で返してあげる必要があります。そのため、
ApplicationControllerに全メソッド共通の例外処理などを簡単には書くことはできません。そのため、個人的には下記のような形式がまだ綺麗な方じゃないかなーと思っています。app/controllers/proto/tasks_controller.rbdef show task = Task.find(params[:id]) response = Protos::FetchTaskResponse.new(task: task.convert_to_message_object) rescue => e response = Protos::Status.new(code: 500, message: e.message) ensure response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data, status: :ok end注釈
注釈1
protoc-gen-swagger というライブラリを介して、 protobuf の定義から OpenAPI 定義ファイルを生成することも可能なようです。
注釈2
show アクションのリクエスト関しては、
/* GET proto/tasks タスク一件の取得 */ message FetchTaskRequest { int32 id = 1; }と表現することもできるのですが、rails の show アクションのデフォルト定義に合わせて、今回は
GET proto/task/:idで取得することにしました。参考文献など
今さらProtocol Buffersと、手に馴染む道具の話
WEB+DB PRESS Vol.108 スキーマ駆動Web API開発
Web API に秩序を与える Protocol Buffers
Protocol Buffers | Google Developers
- 投稿日:2019-12-19T15:15:54+09:00
RubyのGCを動かして、ざっくり理解する
普段プログラムを書いていると、あまり気にすることのないGCの動きについて知りたくなったので、調べた内容をまとめてみました。
GC自体は様々な言語に導入されており、アルゴリズムもそれぞれですが今回はRuby(C Ruby)のGCについて書いていきます。RubyのGCアルゴリズムについて
Rubyでは元来「mark&sweep」というアルゴリズムでGCが動いておりました。
このmark&sweepでは
1. マークフェーズ
2. スイープフェーズ
の2つのフェーズがあります。まずマークフェーズでは、rootオブジェクトから参照のあるオブジェクトのみにマークしていきます。
スイープフェーズではマークのついていない、つまり使われていないオブジェクトを解放していきます。
この2つのフェーズを行うだけのシンプルな手法です。
ですが、この手法ではスイープの際に全てのオブジェクトに対してマークがあるかどうかのスキャンが必要となり、他のすべての処理がブロックされてしまうという欠点がありました。
この現象を「Stop the World」と言います。この欠点を補うために導入されたアルゴリズムが「世代別インクリメンタルGC」です。
世代別インクリメンタルGCは「世代別GC」と「インクリメンタルGC」の合わせ技となっているので、順に解説していきます。世代別GC
世代別GCとはオブジェクトの空間を世代別に分けることによって、GCが行われる範囲を小さくする手法です。
世代には、
- 新しいオブジェクトのみを対象とするマイナーGC
- 古いオブジェクトを含めたすべてのオブジェクトを対象とするメジャーGC
があり、運用時には新しいオブジェクトの寿命が短いことがほとんどであるということを前提に、マイナーGCを高頻度で行います。
そして、メジャーGCではオブジェクトの数が一定の閾値を超えた場合のみ実行されます。
この世代で分けた手法によりmark&sweepのみに比べて処理性能の改善が可能となります。
ただし、古い世代も対象とするメジャーGC実行時のブロック時間が掛かるという問題はまだ残ります。インクリメンタルGC
その欠点を補うのがインクリメンタルGCです。
インクリメンタルGCでは、実行プログラムと細かなGCを交互に行うことで、ブロック時間を短くする事が出来ます。
対象プログラムの総実行時間が短くなるわけではありませんが、これでStop the Worldを回避できます。
プログラムとGCを交互に細かく行うということはオブジェクトの参照関係の変更される可能性がありますが、それを検知する為にライトバリアという手法が採用されています。世代別インクリメンタルGC
上記2つのアルゴリズムを合わせたものを世代別インクリメンタルGCと言い、Rubyではこちらが採用されています。
世代別GCをベースにブロック時間が長いメジャーGCではインクリメンタルGCが実行されています。GCの動きを確認してみる
仕組みはざっくり理解出来たので、GCが実際に実行されるのを確認する為の簡単なプログラムを動かしてみましょう。
環境
- ruby 2.5.1
hoge.rbGC::Profiler.enable puts "プログラム実行前: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}" a = Array.new(10000000) do 'hello' end puts "プログラム実行後: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}" puts "total_time: #{GC::Profiler.total_time.round(3).to_s}ms"変数aに10000000個の
'hello'の文字列が入った配列を作り、その処理中に実行されるGCの動きをRubyの組み込みモジュールのGCとGC:: Profilerを使って結果を出力してみました。
ここではGC.stat(現在のGCのステータス値を出力するメソッド)の結果を、
:minor_gc_count
- マイナーGCが行われた回数
:major_gc_count
- メジャーGCが行われた回数
:heap_free_slots
- 空きスロット数
:heap_live_slots
- 有効な全オブジェクトの数
:old_objects
- マイナーGCでは開放されないマークされたオブジェクト
の5つだけに絞っています。
そして結果はこのようになりました。$ ruby hoge.rb プログラム実行前: {:minor_gc_count=>9, :major_gc_count=>2, :heap_free_slots=>96, :heap_live_slots=>23135, :old_objects=>15229} プログラム実行後: {:minor_gc_count=>21, :major_gc_count=>7, :heap_free_slots=>396, :heap_live_slots=>10016377, :old_objects=>6333071} total_time: 0.301msプログラム実行後を見ると、
- マイナーGC: 12回
- メジャーGC: 5回
行われている様子です。
old_objectsが6333071個にまで増えているのを見ると、生成した変数a内のオブジェクトのほとんどが古いオブジェクトとしてマークされているのが分かりました。手動でマイナーGCを実行してみる
次に
GC.startを使って手動でマイナーGCを実行してみます。hoge.rbGC::Profiler.enable puts "プログラム実行前: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}" a = Array.new(10000000) do 'hello' end GC.start(full_mark: false) # マイナーGCを実行 puts "プログラム実行後: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}" puts "total_time: #{GC::Profiler.total_time.round(3).to_s}ms"マイナーGCを実行するには
GC.start(full_mark: false)を追加すればOKです。
結果を見てみます。$ ruby hoge.rb プログラム実行前: {:minor_gc_count=>9, :major_gc_count=>2, :heap_free_slots=>104, :heap_live_slots=>23126, :old_objects=>15229} プログラム実行後: {:minor_gc_count=>22, :major_gc_count=>7, :heap_free_slots=>788, :heap_live_slots=>10016383, :old_objects=>10016160} total_time: 0.383ms先程と比べて
heap_free_slotsに空きが少し出来ていますが、old_objectsがさらに増えているのを見ると、古いオブジェクトと判断されたものがほとんどでマイナーGCでのメモリの解放対象となるオブジェクトは少なかったのが分かります。
GC実行時間は先程と比べると、おおよそ80msほどかかっています。手動でメジャーGCを実行してみる
次に手動でメジャーGCを実行してみます。
変数a内にあるオブジェクトをメジャーGCの対象にしたいので少しコードを変更しましたhoge.rbGC::Profiler.enable puts "プログラム実行前: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}" a = Array.new(10000000) do 'hello' end a = Array.new(100) GC.start # メジャーGCを実行 puts "プログラム実行後: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}" puts "total_time: #{GC::Profiler.total_time.round(3).to_s}ms"Arrayオブジェクトをの中で
'hello'を大量に生成した後、すぐにnilが100個入ったArrayを変数aに代入してみます。
少し無理やりですが、これではじめに作った変数a内の10000000個のオブジェクトは参照マークが外れてメジャーGCの対象になるはずなので実行してみます。
結果がこちら。$ ruby hoge.rb プログラム実行前: {:minor_gc_count=>9, :major_gc_count=>2, :heap_free_slots=>95, :heap_live_slots=>23137, :old_objects=>15229} プログラム実行後: {:minor_gc_count=>21, :major_gc_count=>8, :heap_free_slots=>6326296, :heap_live_slots=>16377, :old_objects=>16157} total_time: 0.404ms
old_objectsもマイナーGCの時と比べてかなりGC実行前の初期値に近くなっており、古いオブジェクトのほとんどが解放されたのが分かります。
heap_free_slotsも6326296まで増えていますね。最後に
ざっくりとですがRubyのGCのアルゴリズムとサイクルを理解することが出来ました。
実務でメモリがネックとなる問題が起こった際などには、現在は「新しいオブジェクトの寿命が短いことがほとんどである」という前提のアルゴリズムで動いているということだけでも頭にいれておけば、調査の際の手助けとして、上記の例で使ったGCモジュールなどを使って長生きなオブジェクトを大量に作っていないかをチェックすることが出来そうです。
とはいえGCが動く条件などが実行プログラムによって大きく変わることが多く、かつ複雑でもあるのでトライ&エラーを繰り返す必要がありそうです。参考
http://www.atdot.net/~ko1/activities/sasada-ipsj-pro101.pdf
https://ja.wikipedia.org/wiki/%E4%B8%96%E4%BB%A3%E5%88%A5%E3%82%AC%E3%83%99%E3%83%BC%E3%82%B8%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3
https://docs.ruby-lang.org/ja/latest/class/GC.html
https://qiita.com/yuroyoro/items/14ec7079f6574ad74409
- 投稿日:2019-12-19T13:33:20+09:00
Windowsユーザーの私がrbenvでrubyのインストール
1.前提
-Windowsユーザー
-rubyを使いたいmacと違い、windowsでは PCにrbenvを直接インストールできない
準備として、windows内にubuntuという環境を作る必要がある。ここからの話は、windows内というよりは、windows内のubuntu内で起きることである。
(調べ方も当然、"ubuntu rbenv インストール"となる)
(注)rbenvだけだと、どうしてもmacのことばかり出てきてしまうので気をつける。しかし、PC自体の容量は空けとかないと、ここからのインストールの容量が足りないから、Windowsが無関係ってわけではない
(Windows内のubuntuが様々なものをインストールし、大きくなっていくから、ubuntuをかかえるWindowsの容量がないと、かかえきれない)2.実際にインストール開始
2-1.rbenv をインストール(clone)する
git clone https://github.com/rbenv/rbenv.git ~/.rbenv2-2.環境設定 PATHに追加する
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc2-3.rbenvがあるかのように、場所をさぐってみる
~/.rbenv/bin/rbenv initそうすると、rbenvのインストールコマンドを提案してくれる
sudo apt install rbenv
↑これで、rbenvはインストールできますよというメッセージそこから言われた通り、
2-4.rbenvのインストールコマンド
sudo apt install rbenvでインストール
その後、
メッセージででてきた手段でrubyインストール
↓2-5.rubyインストール
rbenv install 2.5.3rubyの構築が完了した
3.エラー対処
途中でエラーすることがある
その際は、3-1.容量の確認
df -hちなみに自分はPC本体の容量がいっぱいいっぱいだった(3-1の実行結果)
:$ df -h Filesystem Size Used Avail Use% Mounted on rootfs 235G 235G 18M 100% / none 235G 235G 18M 100% /dev none 235G 235G 18M 100% /run none 235G 235G 18M 100% /run/lock none 235G 235G 18M 100% /run/shm none 235G 235G 18M 100% /run/user C: 235G 235G 18M 100% /mnt/c(このパターンは稀で、容量不足としては仮想環境に割り当てられるディスク容量が足りないことが多いんだとか)
4.インストールできたか確認
バージョンを聞くコマンド
--versionを利用4-1.rbenvのインストール確認
$ rbenv --versionと入力し、
rbenv 1.0.0などのバージョンが出てくればよい。4-2.rubyのインストール確認
$ ruby --versionと入力し、
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux]などのバージョンが表示されればよい。(参考にさせていただいたサイト)
- 投稿日:2019-12-19T12:40:53+09:00
Slim の変換結果をちょこっと確認するスクリプト
はじめに
この記事は @eggc さんの以下の記事に触発されて書いたもの。
slim ちょっと試したいときには slimrb を使う - Qiita
要するに,テンプレートエンジン Slim で,「こう書いたらこうなる?」というのをコマンドラインでサクッと確かめるには,slim gem によって導入される slimrb というコマンドを使えばよい,ということ。
この方法は,とにかくシンプルで,二三度やってみれば覚えられるから,「ええと,どうやるんだっけ?」と悩む必要がない1。
ただ,これだとややこしい Slim コードを少しずつ修正しながら結果を確認するのが面倒くさい。
試行錯誤をもっと楽にしたい,というのが本記事の趣旨。スクリプトで変換する
slim のほか,listen というディレクトリー監視の gem が必要なので
gem install slim listenで入れておく。
そして以下のスクリプトを走らせる。
require "listen" require "slim" this_file = File.expand_path(__FILE__) Slim::Engine.set_options pretty: true Listen.to(__dir__) do |(m, *_),| next unless m == this_file IO.read(this_file) =~ /^__END__$/ puts Slim::Template.new{$'}.render, "---" end.start sleep __END__ - 3.times div わん
__END__の下に Slim コードが書かれている。これだけでは何も起きないが,試しにこの Slim コードを変更してみよう。改行を足すだけでもいい。
変更して保存すると,変換した結果の HTML と区切り線---がターミナルに表示される。
変更して保存するたびに表示される。Slim コードは別ファイルにしてもいいのだが,全部を 1 ファイルにまとめてみた。
なお,Slim の変換のモードとして,
Slim::Engine.set_options pretty: trueのように,改行・インデントが入る形式を指定している。
改行・インデントを入れない形式にしたければ,この行をコメントアウトすればいい(こういう変更を行った場合はスクリプトを一度終了して再度動かすこと)。コードの解説
ファイルの変更を監視するため,listen を使う。
Listen.toは,引数に監視ディレクトリーのパスを与えておくと,そこでファイルの変更・追加・削除があった場合に,それらのファイルのパスをブロックパラメーターとしてブロックを評価してくれる。ブロックパラメーターは順に,①変更ファイルのパスの配列,②追加ファイルのパスの配列,③削除ファイルのパスの配列。
なんで配列なのかというと,一定時間おきにチェックして,前回チェックとの差分を見るわけなので,一般には複数のファイルが変更・追加・削除されうるから。監視を始めるには
Listen.toの返り値に対し,startしてやらなければならない。ブロックパラメーターが
|(m, *_),|と変な形をしているけど,これは第一ブロックパラメーター以外を捨てる,ということと,第一ブロックパラメーターについては,配列の先頭のみをmに当てて,残りを_で受けて捨てることを意味している。そして,スクリプトファイル自身をテキストデータとして取得し,正規表現
/^__END__$/にマッチする箇所を探して,そこ以降のテキストを$'で取り出している。Slim コードを HTML に変換するには,
Slim::Template.newのブロックに Slim コードを与え,renderすればいい。なお,スクリプト末の
sleepが無いと,スクリプトは一瞬で終わってしまう。
コマンド名が思い出せなければ
gem spec slimで slim gem の情報を表示し,executablesのところを見ればslimrbと出ている。 ↩
- 投稿日:2019-12-19T12:14:07+09:00
自動メール送信作ってみた
アドベントカレンダークリスマスイブ担当koniaです。
自己紹介
この会社に入り本格的にエンジニアとして働き出して早1年目、まだまだ足りないもの感じながらしごかれて最後には星になります。そんな若輩者の拙い文章ですが、ちょっとした時間に読んでいただき1つでも為になったら心中でワルツが踊り出します。
ふとした依頼
さて、自己紹介はこれくらいにして最近サークルの後輩からこういう依頼が来ました。
「最近イベントとかするんですけど、そのメールを自動で返信してくれるシステム的なの欲しいんですよ…」
「サクッと作れないですかね」
お、これは作るしかないと。いろんなサービスが今や横行していますが、なんかちょっとエンジニアチックに作りたいなと思って色々みていました。
そこで見つけたのが、fetchmailというOSSです。リモートのメールサーバからメールを取得し、 これをローカル (クライアント) マシンの配送システムに転送します。 受け取ったメールは普通のメールユーザエージェントで扱うことができます。
https://linuxjm.osdn.jp/html/fetchmail/man1/fetchmail.1.html
こちらを参照と言ったようにメールを扱うOSSがありましたのでこちらを使用していきます。
自動メール送信
OSS : fetchmail
OS : Ubuntu 18.04
言語 : Ruby
OS、言語選定については後輩がわかるものに揃えた為、特に理由があったわけではないので他でもできます。fetchmail設定ファイルについて
#86400秒(24時間)ごとに起動する set daemon 86400 defaults uidl no mimedecode no rewrite # メールを渡すプログラムのフルパス mda /home/name/process/textProcessing.rb keep #メールサーバの情報 poll mail.example.jp protocol POP3 username example@example.jp password xxxxxxxx ssl特定の時間がきた場合、textProcessing.rbが起動するようになってます。
次にtextProcessing.rbのコードを示します。
このコード内部で行なっているのはイベント登録に来たメールからデータを取得し、CSV吐き出しにしています。(今回はDBを用いていません。)#! /usr/bin/ruby # coding: utf-8 #------------------------------------------------- # Ruby script to get a mail via alias of postfix. #------------------------------------------------- require 'mail' require 'csv' class GetMail def initialize #csvの格納場所 @out_csv = "/home/konia/deskTop/mail.csv" end # イベント登録した際の学生情報メールを改行とコロンによって情報を配列化 def execute CSV.open(@out_csv, "a") do |f| mail = Mail.new($stdin.read.gsub(/\r?\n/,"\r\n")) mail_sender = mail.from.first mail_subject = mail.subject.to_s mail_body = mail.body.to_s mail_body = mail_body.force_encoding("utf-8") mail_array = mail_body.scan(/:\s*([^\r]*)/) if mail_array.length >= 7 && mail_sender == "hoge@gmail.com" && mail_subject.scan("event").empty? date = mail_array[6].to_s.scan(/(\d+[^\(])/) time = date[3] date.slice!(3,2) mail_array[6] = date.join mail_array[7] = time # 配列化した情報をcsvで取り込む f << mail_array.flatten end end rescue => e $stderr.puts "[#{e.class}] #{e.message}" e.backtrace.each{|trace| $stderr.puts "\t#{trace}"} exit 1 end end exit unless $0 == __FILE__ GetMail.new.executeそこから次はメールを送信する際の処理を下記に示します。
CSVから特定のデータを取得し、それを整形し特定の日付になった際にメール送信を行う。#! /usr/bin/ruby # coding: utf-8 #------------------------------------------------- # Ruby script to get a mail via alias of postfix. #------------------------------------------------- require 'mail' require 'csv' require 'time' class TextProcess def initialize @@from_mail = '送信者' @@to_mail = '受信者' @csv = "/home/hashi/deskTop/mail.csv" end # csvファイルから時間帯のデータを取ってき、イベント3日前と前日についての学生をまとめる def mailProcess student_info = {} flg_3 = i = 0 CSV.foreach(@csv) do |cs| date = Time.strptime(cs[6].to_s,'%Y年%m月%d日') today = Time.now three_date_before = date - 259200 two_date_before = date - 172800 one_date_before = date - 86400 if three_date_before <= today && today < two_date_before flg_3 = 1 student_info[i] = cs i += 1 elsif one_date_before <= today && today < date sendMail(2,cs) end end if flg_3 == 1 processed_text = textProcess(student_info) sendMail(1,processed_text) end end # 上記のイベント前に合致する学生情報をメールの本文に作成する def textProcess(tmp) i = 0 info = "" for j in 0...tmp.length do info << "氏名:#{tmp[j][0].to_s}\nメールアドレス:#{tmp[j][4].to_s}\n会場:#{tmp[j][5].to_s}\n日時:#{tmp[j][6].to_s+tmp[j][7].to_s}\n\n\n" i += 1 end return info end # 作成したメールについて送信を行う def sendMail(flg,tmp) if flg == 2 mail = Mail.new do from @@from_mail to @@to_mail cc @@to_mail2 bcc @@to_mail3 subject "前日のお知らせです" body "氏名:#{tmp[0].to_s}\nメールアドレス:#{tmp[4].to_s}\n会場:#{tmp[5].to_s}\n日時:#{tmp[6].to_s+tmp[7].to_s}" end elsif flg == 1 mail = Mail.new do from @@from_mail to @@to_mail cc @@to_mail2 bcc @@to_mail3 subject "3日前のお知らせです" body tmp end end mail.charset = 'utf-8' mail.delivery_method(:smtp, address: "送信サーバ", port: ポート番号, domain: "ドメイン名", authentication: :login, user_name: "メールアドレス", password: "送信サーバのパスワード", enable_starttls_auto: false ) mail.deliver end end exit unless $0 == __FILE__ TextProcess.new.mailProcess以上となります。OSS使って書いてみると色々と気づくものもありますが今の時代なんでもそろってるからそっちを使った方が楽なのは間違いないですね?
- 投稿日:2019-12-19T12:08:49+09:00
Rubyチートシート(一部)
徐々に増やしていく予定。
実行時の引数を取得
# 引数の数が2以外ならNG if ARGV.size == 2 folder_pass = ARGV[0] str = ARGV[1] else puts "ruby #{__FILE__} フォルダパス 検索条件 の形式で実行" exit 1 end文字列
文字列が含まれているか
真偽を返す
str.include?("r")正規表現に一致するか
str =~ /<(\w+)>/一致した部分を置換
# "\"(円マーク)を"/"に変換 folder_pass = ARGV[0].gsub("\\", "/")正規表現も可能
# 第2引数が空の場合は、一致した部分の削除になる str.gsub(/<(\w+)>/, "")ディレクトリ
指定パスにディレクトリが存在するかチェック
if FileTest.directory?(folder_pass) func(folder_pass) else puts("フォルダパスが正しくありません") raise ArgumentError endディレクトリ配下のファイルをすべて検索
- パスが存在しない場合はループに入らない(エラーも出ない)
- フォルダの区切り文字は"
/(スラッシュ)"。"\(円マーク)" だと検索できない# testフォルダ以下のすべてのファイルを検索 Dir.glob("#{test}/**/*") do |pass| next if FileTest.directory?(pass) # ディレクトリは無視 puts pass endディレクトリ配下のファイル数を数える
ディレクトリはカウントしない
file_num = Dir.glob("#{test}/**/*.*").countファイル
ファイルを1行ずつ読み込む
# test.txt はUTF-8 で書かれている File.foreach("test.txt", :external_encoding => "UTF-8") do |line| line.chomp! # 改行削除 next if (line == "") # 空行を無視 puts line end例外
例外を発生させる
raise ArgumentError
- 投稿日:2019-12-19T12:06:45+09:00
Capistranoで自動デプロイ時に起きるエラーについて〜AWS〜
はじめに
Capistranoを使って、AWSに自動デプロイをかけた時に出るエラーのお話
本件は主に再起動時に起きるエラーです
最後にデプロイ時に確認する事項をまとめていますので、そちらも軽くご参照いただければと思いますErroe内容
bundle exec cap production deploy実行後
terminalMac_ikuy:"hogehoge" User$ bundle exec cap production deploy #<Thread:0x00007f8b2b50bb18@/Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:10 run> terminated with exception (report_on_exception is true): Traceback (most recent call last): 1: from /Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:11:in `block (2 levels) in execute' /Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:15:in `rescue in block (2 levels) in execute': Exception while executing as ec2-user@18.177.78.202: Authentication failed for user ec2-user@xx.xxx.xxx.xxx (SSHKit::Runner::ExecuteError) (Backtrace restricted to imported tasks) cap aborted! SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@xx.xxx.xxx.xxx: Authentication failed for user ec2-user@xx.xxx.xxx.xxx Caused by: Net::SSH::AuthenticationFailed: Authentication failed for user ec2-user@xx.xxx.xxx.xxx Tasks: TOP => rbenv:validate (See full trace by running task with --trace)こんな感じのエラーが出ます
今回の話の結論からいうと、SSHキーが消えてしまっているので、.sshにて下記コマンドを入力しますterminal$ cd ~/.ssh .ssh $ ssh-add ~/.ssh/hoge.pemhoge.pemにはインスタンスに対応したpemキーを入力してください
Error解決しないときは
エラー文に慣れない方はなんかいっぱい書いてあってうわぁって思うかもしれませんが、ポイントは"Caused by"です
これのせいで、ってことですね
"Exception while executing"を直訳すると「実行中の例外」です
何か例外が起きてしまったんだなってことですね
AWSへのデプロイ時の基本注意点としては、
- mysql/nginexの再起動
- unicorn killの確認
です
前者は[ec2-user@ip-xxx ~]$ sudo service nginx restart [ec2-user@ip-xxx ~]$ sudo service mysqld restart後者は
[ec2-user@ip-xxx ~]$ ps aux | grep unicorn ec2-user 4223 0.0 0.0 11xxxx master ec2-user 4175 0.0 0.0 11xxxx ~~~~省略~~~~ ec2-user 4236 0.0 0.0 11xxxx ~~~~省略~~~~ [ec2-user@ip-xxx ~]$ kill 4223 #masterのNO.上記ができているにも関わらずデプロイできないときは、インスタンスを一度停止しましょう
"停止"が完了したら"開始"をしましょう
大概のエラーはこれでで解決できます。まとめ
解決方法のまとめです
- "Net::SSH::AuthenticationFailed: Authentication failed"が出たときはsshキーが消えている可能性があるので、addしてやる
- mysql/nginexの再起動
- unicornのkill
- インスタンスの再起動
- 投稿日:2019-12-19T11:26:13+09:00
seleniumでchromedriverのバージョンエラーが出たときの対処法
背景
seleniumを実行しようとしたら突然以下のエラーが表示されchromeが起動しない。
起動しないというかブラウザウィンドウは一瞬起動しようとするけど即落ちてエラーになる。session not created: This version of ChromeDriver only supports Chrome version 77 (Selenium::WebDriver::Error::SessionNotCreatedError)chromedriverがChromeのバージョンに対応していませんよとのこと。
対処してた環境(ざっくり)
- Mac Mojave
- chrome(79.xxxxxxx)
- selenium + ruby
- chromedriver
対処
1. 以下のコマンドでchromedriverの在り処を把握しておく
$ which chromedriver(多分
/usr/local/bin/chromedriverにあると思います )2. インストールされているchromeのバージョンを確認
chrome > 環境設定 > chromeについて で確認できます
3. chromeのバージョンに対応したchromedriverをダウンロード
以下のサイトでchromeのバージョンにあったchromedriverをダウンロードしましょう
http://chromedriver.chromium.org/downloadsマスターバージョンがあっていれば大丈夫です。多分(自分はそれで動きました)。
4. ダウンロードしたバイナリを設定する
zipファイルがダウンロードされるので解凍。
解凍するとchromedriverが表示されるので、それを手順1で確認した既存のchromedriverに上書きしましょう。
(※解凍先のディレクトリがDownloadディレクトリである想定)$ mv ~/Downloads/chromedriver /usr/local/bin/上書き完了!
これで動くはず。
その他
当方の環境では selenium + ruby でやっていますが別にrubyじゃなくても同じような手順で解決できるんじゃね?と素人目に思っている次第です。はい。
参考にした記事
https://qiita.com/H2O_HoriHori/items/ad7f9855cfc20b995c42
https://qiita.com/y-agatsuma/items/ea2c9845ee0a931d5c9c
- 投稿日:2019-12-19T11:26:13+09:00
[selenium]chromedriverのバージョンエラーが出たときの対処法
背景
seleniumを実行しようとしたら突然以下のエラーが表示されchromeが起動しない。
起動しないというかブラウザウィンドウは一瞬起動しようとするけど即落ちてエラーになる。session not created: This version of ChromeDriver only supports Chrome version 77 (Selenium::WebDriver::Error::SessionNotCreatedError)chromedriverがChromeのバージョンに対応していませんよとのこと。
対処してた環境(ざっくり)
- Mac Mojave
- chrome(79.xxxxxxx)
- selenium + ruby
- chromedriver
対処
1. 以下のコマンドでchromedriverの在り処を把握しておく
$ which chromedriver(多分
/usr/local/bin/chromedriverにあると思います )2. インストールされているchromeのバージョンを確認
chrome > 環境設定 > chromeについて で確認できます
3. chromeのバージョンに対応したchromedriverをダウンロード
以下のサイトでchromeのバージョンにあったchromedriverをダウンロードしましょう
http://chromedriver.chromium.org/downloadsマスターバージョンがあっていれば大丈夫です。多分(自分はそれで動きました)。
4. ダウンロードしたバイナリを設定する
zipファイルがダウンロードされるので解凍。
解凍するとchromedriverが表示されるので、それを手順1で確認した既存のchromedriverに上書きしましょう。
(※解凍先のディレクトリがDownloadディレクトリである想定)$ mv ~/Downloads/chromedriver /usr/local/bin/上書き完了!
これで動くはず。
その他
当方の環境では selenium + ruby でやっていますが別にrubyじゃなくても同じような手順で解決できるんじゃね?と素人目に思っている次第です。はい。
参考にした記事
https://qiita.com/H2O_HoriHori/items/ad7f9855cfc20b995c42
https://qiita.com/y-agatsuma/items/ea2c9845ee0a931d5c9c
- 投稿日:2019-12-19T10:39:49+09:00
.nill , .empty, .blank, .present?の違い
Ruby on Railsで値の存在またはオブジェクトの状態を確認するために使用できるメソッドがいくつかあります。 Rubyには.nilと.empty ? があります。Rails ActiveSupportには.blankと.present?があります。
nil?
.nil? ObjectクラスのRubyメソッドです。 すべてのクラスを受け継ぐかのオブジェクトのクラス#nil? 任意のオブジェクトで使用できます。 Nilの場合はtrue、それ以外の場合はfalseを返します。 これは、空の文字列がnilではなく、空の配列がnilではないことを意味します。
nil.nil? => true "".nil? => false [].nil? =>falseempty?
.emptyは、オブジェクトの長さ== 0を意味します。このメソッドは、文字列、配列、ハッシュで使用できます。
"".empty? => trueただし、空白の場合、改行またはタブ.emptyはfalseを返します。 空白、改行またはタブも文字としてカウントされます。
" ".empty? => false "\t\n".empty? => falseArray ,hashes, set の場合、要素がない場合はtrueを返します。
[].empty? => true {}.empty? => trueblank?
.blank? ActiveSupportのRails method です。
これは、nil、false、empty、empty stringに対してtrueを返します。nil.blank? => true false.blank? => true "".blank? => true " ".blank? => true [].blank? => true {}.blank? => truepresent?
.present?も Rails method です。 .blank?の反対です.
[].present? => false " ".present? => false false.present? => false [1, 2, 3, 4, 5].present? => trueこれがお役に立てば幸いです :)
- 投稿日:2019-12-19T06:17:49+09:00
VSCodeの拡張機能でRailsと仲良くなる
はじめに
こちらは Classi Advent Calendar 19日目の記事です。
こんにちは、Classi Webエンジニアの@hakshuです。皆さんはRailsを書く時エディタは何を使っていますか?
RubyMine?Vim?Emacs?VSCode?Atom?さくらエディタ?自分はフロント/サーバー関わらず、JavaScriptやTypeScriptなどを書くことが多かったので、Visual Studio Code、いわゆるVSCodeを愛用しています。
現在のプロジェクトでは、フロントエンド(Angular)/サーバーサイド(Rails)のどちらも書いており、いちいちエディタを切り替えるのも面倒で、使い慣れていることもあるので、VSCodeを使っています。
TypeScriptもVSCodeもMicrosoft製ということもありますし、静的型付けのため、補完はとても効くので、サクサク書いていくことができます。
ただ、Rubyについては、動的型付けのため、補完をいい感じに効かせるには拡張機能などでカバーが必要です。
なので、今回は自分がMacのVSCodeに入れている拡張機能やその設定の紹介をしていこうと思います。
拡張機能
Ruby・Rails
Ruby
Ruby言語サポートです。Rubyのコードを書き始める前にこれを入れましょう。
文法の最低限のインテリセンスが有効になります。
他にもrbenvやasdfなどでのrubyバージョン切り替えにVSCodeを対応させたり、RubyCopなどでのLint,Formatに対応したりしています。
Settings.json 設定例
"ruby.useBundler": true, // VSCodeが実行するgem関連のコマンドをbundle execで実行するかどうか "ruby.lint": { "rubocop": { // RuboCop, Standard or Reek "useBundler": true // bundlerでインストールしているgemを使うか否か }, }, "ruby.format": "rubocop" // formatに使うgemを指定(RuboCop, Standard, Rufo or RubyFMT)。bundlerでインストールしたgemを使うかどうかは、ruby.useBundler依存
VSCode Ruby
上記拡張機能「Ruby」を入れると自動でインストールされます。ruby・erbのシンタックスハイライトが有効になります。
Rails
その名の通りRailsサポートの拡張機能です。
assetやタグのsnippetを提供しています。
また、
Opt + .で関連ファイルにナビゲーションしてくれます。
他にもRailsのビルトインオブジェクトにフォーカスを当てて、Opt + F1でRailsのドキュメントを確認することや、自作クラスの定義元に飛ぶことが可能になります。
Ruby Solargraph
上記Rails拡張機能を入れるとインストールされます。Rubyのインテリセンスを強化できます。
設定例
Solargraphを使えるようにするには主に3つの方法があります。
- gemをglobal installする
- Gemfileに追加し、bundlerでinstallする
- docker containerなど外部サーバーとして動かす
1. gemをglobal installする場合
solargraphをインストールします。
$ gem install solargraph # グローバルなgemのdocumentを生成します。 $ yard gemssettings.jsonは以下のように設定しましょう。
"solargraph.useBundler": false2. bundlerでinstallする場合
Gemfileにsolargraphを追加してインストールします。
gem 'solargraph', group: :development$ bundle install # プロジェクトにインストールしているgemのdocumentを生成します $ bundle exec yard gemssettings.jsonは以下のように設定しましょう。
"solargraph.useBundler": true1と2の共通設定
solargraphの設定ファイルを作成します。
設定内容については以下に解説があるので、プロジェクトに合わせて設定しましょう。
Configuration$ solargraph config .Solargraph0.34.0以降で、Railsの補完を効かせるには、以下コマンドをRailsプロジェクトのルートディレクトリで実行しましょう。
ActiveRecordなど、Railsのコア部分のgemはSolargraphが使用しているYARDではなく、RDocを使用しているため、このコマンドでRDocからYARDへの変換をしています。$ solargraph bundleその後こちらのファイルをプロジェクトに追加しましょう。
Solargraphが静的解析でRailsアプリを解析しきれない部分とのギャップを埋めるために必要となります。
参考ドキュメント※Solargraph0.34.0以前の場合は
.solargraph.ymlのrequireに以下を追加する必要があります。solargraph.ymlrequire: - actioncable - actionmailer - actionpack - actionview - activejob - activemodel - activerecord - activestorage - activesupport3. Dockerコンテナなど外部サーバーとして立ち上げる場合
まずは以下のようなSolargraphを起動するためのDockerfileをプロジェクトのルートに追加しましょう。
FROM ruby:2.6.4-alpine3.10 RUN apk update \ && apk add --update make gcc g++ # Install gems WORKDIR /home/solargraph COPY Gemfile Gemfile.lock ./ RUN gem install bundler RUN bundle install --jobs=4 RUN gem install solargraph yard # Generate gem documentation RUN yard gems # Convert rails documentation RUN solargraph bundle # Solargraph server port EXPOSE 7658 # Run Solargraph CMD [ "solargraph", "socket", "--host=0.0.0.0", "--port=7658" ]このDockerfileでimageを作成し、containerを起動します。
# build image $ docker build -t vscode-solargraph:latest . # run container $ docker run -d --rm -p 8091:7658 --name solargraph vscode-solargraphこちらのcontainerを使うようsettings.jsonを次のように設定します。
設定後、VSCodeを再起動すると、solargraphが使えるようになります。"solargraph.transport": "external", "solargraph.externalServer": { "host": "localhost", "port": 8091 }
Ruby on Rails
Railsのスニペットを提供してくれます。
solargraphのインテリセンスだけでは、RailsのAPI補完が不十分なので、こちらで補っています。
Rails DB Schema
DB Schemaに沿って、モデルのカラム名を補完してくれます。
Rails Go to Spec
Cmd + shift + yで実装ファイルとテストファイルを行き来できます。endwise
endを自動挿入してくれます。
Rainbow End
endを色分けする拡張機能です。
do~endがネストする時に見分けにくかったので、入れています。
その他Ruby・Railsに限らない拡張機能
indent-rainbow
インデントに応じて色分けしてくれます。
Material Icon Theme
エクスプローラー内のフォルダやファイルアイコンなどが見やすくなるテーマです。
Swagger Viewer
VSCode上でSwaggerファイルのプレビューが可能になります。最後に
以上、拡張機能とその設定でした。
ぜひ気になったものがあったら使ってみてください。明日は @yosuke_ohara さんです。お楽しみに!
- 投稿日:2019-12-19T02:08:34+09:00
画像の複数投稿??プレビュー表示??え??
〇〇エキスパートにて
どうも、pirikaraです。
チーム開発にて某メルカリのクローンサイトを作成中、商品出品の画像投稿で詰まりました。
プレビューは表示されるのにデータが入ってない・・・・・・
プレビュー消したのにデータが残っている・・・・・・
など散々格闘したので、参考になればと思って書きました。10月からプログラミング学習を開始した弱々エンジニアですので、お手柔らかによろしくお願い致します。
仕様
- 画像を10枚投稿できる
- 投稿した画像は1枚ずつプレビューされる
- 5枚目以降は2段目にプレビュー表示される
- 削除を押すとプレビューから消える
- 追加・削除したプレビューとfile_fieldの中身が同期している
やってみよう
まずはrails newで適当なアプリケーションを立ち上げます。
今回は適当にsample_appとしました。
databaseはmySQLを使用します。今回は商品出品なので、ItemモデルとImageモデルを作成。
CarrierWaveとminimagickのgemをインストールして、
アソシエーションを組みます。item.rbclass Item < ApplicationRecord has_many :images, dependent: :destroy accepts_nested_attributes_for :images, allow_destroy: true endimage.rbclass Image < ApplicationRecord belongs_to :animal, optional: true mount_uploader :image_url, ImageUploader end※accepts_nested_attributes_forは以下の記事を参考にしました
Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for)databaseのカラムを作成します。
今回はitemはnameのみ、imageはitem_idでitemと紐付けて、image_urlカラムに画像ファイル名が保存されるようにしました。
フォームを作るよ
できました。
items/new.haml.main %section.main__block = form_with model:@item, local:true do |f| %h2.sell__block__head 商品の情報を入力 .sell__block__form .sell__block__form__upload %h3.sell__block__form__upload__head 出品画像 %span.require 必須 %p 最大10枚までアップロードできます #image-box-1 .item-num-0#image-box__container = f.fields_for :images do |i| .input-area = i.file_field :image, type: 'file', name: "item[images_attributes][][image_url]", value:"", style: "display:none", id:"img-file" %label{for: "img-file"} %i.fas.fa-camera .sell__block__form__name .form-group__name %label 商品名 %span.require 必須 %div = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name" .sell__block__form__btn %div = f.submit "出品する",class: "btn-default__btn-red"コードはこんな感じ。
items/new.haml#image-box-1 .item-num-0#image-box__containerこの間にプレビューのコードがJavaScriptで挿入される感じで実装していきます。
JQueryを書くよ
まず、作業を以下の4つに分けました。
1. プレビューの表示
2. プレビューの削除
3. プレビューの複数表示
4. ドラッグ&ドロップへの対応順番に作業していきます。
1. プレビューの表示
できました。
コードは以下の通りです。
new_item.js$(function(){ //querySelectorでfile_fieldを取得 var file_field = document.querySelector('input[type=file]') //fileが選択された時に発火するイベント $('#img-file').change(function(){ //選択したfileのオブジェクトをpropで取得 var file = $('input[type="file"]').prop('files')[0]; //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む var fileReader = new FileReader(); //読み込みが完了すると、srcにfileのURLを格納 fileReader.onloadend = function() { var src = fileReader.result var html= `<img src="${src}" width="114" height="80">` //image_box__container要素の前にhtmlを差し込む $('#image-box__container').before(html); } fileReader.readAsDataURL(file); }); });今の状態ではプレビューが削除できないため、削除できるようにします。
2. プレビューの削除
まずはhtml部に削除ボタンを追加します。
new_item.jsvar html= `<div class='item-image' data-image="${file.name}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="114" height="80" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--delete'>削除</div> </div> </div>`削除ボタンを押すと発火するイベントを作成し、プレビューが削除できるようにします。
new_item.js$(document).on("click", '.item-image__operetion--delete', function(){ //プレビュー要素を取得 var target_image = $(this).parent().parent() //プレビューを削除 target_image.remove(); //inputタグに入ったファイルを削除 file_field.val("") })file_fieldに画像が入ったと同時にプレビューが出て、プレビュー削除と同時にfile_fieldに入った画像が消えました。
3. プレビューの複数表示
ここからが本番です。
大まかな流れとしては、
①DataTransferオブジェクトを作成し、データを格納する箱とする
②each文を用いて、DataTransferオブジェクトの箱にfileを追加していく。
③箱の中身をfile_fieldに代入して、複数データを持たせる
④ついでeach文の中でプレビューを追加していく。
って感じです。DataTransferオブジェクトに関しては、下記のサイトを参考にしました。
Javascriptプログラミング講座画像を挿入すると、DataTransferオブジェクト内のfiles: FileListにデータが追加されていきます。
それではコードを書いていきましょう。
new_item.js$(function(){ //DataTransferオブジェクトで、データを格納する箱を作る var dataBox = new DataTransfer(); //querySelectorでfile_fieldを取得 var file_field = document.querySelector('input[type=file]') //fileが選択された時に発火するイベント $('#img-file').change(function(){ //選択したfileのオブジェクトをpropで取得 var files = $('input[type="file"]').prop('files')[0]; $.each(this.files, function(i, file){ //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む var fileReader = new FileReader(); //DataTransferオブジェクトに対して、fileを追加 dataBox.items.add(file) //DataTransferオブジェクトに入ったfile一覧をfile_fieldの中に代入 file_field.files = dataBox.files var num = $('.item-image').length + 1 + i fileReader.readAsDataURL(file); //画像が10枚になったら超えたらドロップボックスを削除する if (num == 10){ $('#image-box__container').css('display', 'none') } //読み込みが完了すると、srcにfileのURLを格納 fileReader.onloadend = function() { var src = fileReader.result var html= `<div class='item-image' data-image="${file.name}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="114" height="80" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--delete'>削除</div> </div> </div>` //image_box__container要素の前にhtmlを差し込む $('#image-box__container').before(html); }; //image-box__containerのクラスを変更し、CSSでドロップボックスの大きさを変えてやる。 $('#image-box__container').attr('class', `item-num-${num}`) }); }); //削除ボタンをクリックすると発火するイベント $(document).on("click", '.item-image__operetion--delete', function(){ //プレビュー要素を取得 var target_image = $(this).parent().parent() //プレビューを削除 target_image.remove(); //inputタグに入ったファイルを削除 file_field.val("") }) });image-box__containerクラスを持つdivタグのクラスをプレビューBOXの数に応じて変更し、
CSSをによってドロップボックスが小さくなるようにしました。
画像が5枚になったら、cssの指定でドロップボックスのwidthを100%に戻します。
するとドロップボックスが2行目に落ちてきます。
10枚になったらドロップボックスを消してあげます。items/new.haml.item-num-0#image-box__containeritem_new.scss.item-num-0{ width: 100%; } .item-num-1{ width: 491px; } .item-num-2{ width: 363px; } .item-num-3{ width: 234px; } .item-num-4{ width: 106px; } //下段のfile_field .item-num-5{ width: 100%; } .item-num-6{ width: 491px; } .item-num-7{ width: 363px; } .item-num-8{ width: 234px; } .item-num-9{ width: 106px; } }できました。
プレビューの追加に応じて、「ファイルを選択」欄のファイル数が増えていくようになりました。しかしこのままでは以下のコードによって、削除ボタンを押すとfile_fieldに格納されている全てのデータが消えてしまう状態です。
なのでコードを書き換えていきます。new_item.js//変更前 $(document).on("click", '.item-image__operetion--delete', function(){ //プレビュー要素を取得 var target_image = $(this).parent().parent() //プレビューを削除 target_image.remove(); //inputタグに入ったファイルを削除 file_field.val("") })new_item.js//変更後 //削除ボタンをクリックすると発火するイベント $(document).on("click", '.item-image__operetion--delete', function(){ //削除を押されたプレビュー要素を取得 var target_image = $(this).parent().parent() //削除を押されたプレビューimageのfile名を取得 var target_name = $(target_image).data('image') //プレビューがひとつだけの場合、file_fieldをクリア if(file_field.files.length==1){ //inputタグに入ったファイルを削除 $('input[type=file]').val(null) dataBox.clearData(); console.log(dataBox) }else{ //プレビューが複数の場合 $.each(file_field.files, function(i,input){ //削除を押された要素と一致した時、index番号に基づいてdataBoxに格納された要素を削除する if(input.name==target_name){ dataBox.items.remove(i) } }) //DataTransferオブジェクトに入ったfile一覧をfile_fieldの中に再度代入 file_field.files = dataBox.files } //プレビューを削除 target_image.remove() //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更 var num = $('.item-image').length $('#image-box__container').show() $('#image-box__container').attr('class', `item-num-${num}`) })ドラッグ&ドロップへの対応
ドロップエリアをクリックした時にファイルが入る&プレビューが表示される実装が完了したので、ドラッグ&ドロップでも画像が投稿できるようにコードを追加します。
ドラッグ時にドロップエリアに影がつくようにしてみました。
プレビュー表示、file_fieldへのfile追加に関してはクリックによるものとほとんど同じです。new_item.jsvar dropArea = document.getElementById("image-box-1"); //loadイベント発生時に発火するイベント window.onload = function(e){ //ドラッグした要素がドロップターゲットの上にある時にイベントが発火 dropArea.addEventListener("dragover", function(e){ e.preventDefault(); //ドロップエリアに影がつく $(this).children('#image-box__container').css({'border': '1px solid rgb(204, 204, 204)','box-shadow': '0px 0px 4px'}) },false); //ドラッグした要素がドロップターゲットから離れた時に発火するイベント dropArea.addEventListener("dragleave", function(e){ e.preventDefault(); //ドロップエリアの影が消える $(this).children('#image-box__container').css({'border': '1px dashed rgb(204, 204, 204)','box-shadow': '0px 0px 0px'}) },false); //ドラッグした要素をドロップした時に発火するイベント dropArea.addEventListener("drop", function(e) { e.preventDefault(); $(this).children('#image-box__container').css({'border': '1px dashed rgb(204, 204, 204)','box-shadow': '0px 0px 0px'}); var files = e.dataTransfer.files; //ドラッグアンドドロップで取得したデータについて、プレビューを表示 $.each(files, function(i,file){ //アップロードされた画像を元に新しくfilereaderオブジェクトを生成 var fileReader = new FileReader(); //dataTransferオブジェクトに値を追加 dataBox.items.add(file) file_field.files = dataBox.files //lengthで要素の数を取得 var num = $('.item-image').length + i + 1 //指定されたファイルを読み込む fileReader.readAsDataURL(file); // 10枚プレビューを出したらドロップボックスが消える if (num==10){ $('#image-box__container').css('display', 'none') } //image fileがロードされた時に発火するイベント fileReader.onload = function() { //変数srcにresultで取得したfileの内容を代入 var src = fileReader.result var html =`<div class='item-image' data-image="${file.name}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="114" height="80" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--delete'>削除</div> </div> </div>` //image-box__containerの前にhtmlオブジェクトを追加 $('#image-box__container').before(html); }; //image-box__containerにitem-num-(変数)という名前のクラスを追加する $('#image-box__container').attr('class', `item-num-${num}`) }) }) }databaseにも無事保存されましたね。めでたしめでたし。
終わりに
上記のコードを参考にして、ご自身のアプリで挙動を確かめてみてください。
無事に動くと良いですね。問題点としては
『同じ画像を貼り付けた場合に、削除を押すと複数同時に消えてしまう』
というエラーを抱えていることでしょうか・・・・・・(大問題)直したら追記します。
直せる強強エンジニアのかたいらっしゃいましたら教えてください。以上、初投稿でした。
お目通しいただきありがとうございました。















































