20191219のRubyに関する記事は27件です。

[WIP]ActiveRecordを使い、複数テーブルへBULK INSERTする方法

目的

  • Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
  • 今後、railsを学ぶ方に向けての参考に役立てる。

前提条件

  • 実行環境

    • Ruby 2.5.1
    • Rails 5.2.3
    • MySQL 5.7
  • gem

  • 想定

    • 大量のデータ(CSVファイル)をseedで一括で取り込みたい

手順

  1. CSVファイルを設置する
  2. seeds.rbにCSVをBULK INSERTするコードを記述
  3. 各modelにオプションを設定
  4. 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.rb

2. 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'))

end

3. 各modelにオプションを設定

app/models/hoge.rb
class 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
end
app/models/fuga.rb
class 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
end

4. rails db:seed(完)

bash
rails db:seed

関連URL

所感

久しぶりに書いた。業務でCSVインポートの処理を実装するの何気に多い。
後で再インプットも兼ねて解説コメント丁寧に書く。
気力があれば過去記事のメンテナンスしていく

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

[WIP]ActiveRecordを使って複数テーブルへBULK INSERTする方法

目的

  • Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
  • 今後、railsを学ぶ方に向けての参考に役立てる。

前提条件

  • 実行環境

    • Ruby 2.5.1
    • Rails 5.2.3
    • MySQL 5.7
  • gem

  • 想定

    • 大量のデータ(CSVファイル)をseedで一括に取り込みたい

手順

  1. CSVファイルを設置する
  2. seeds.rbにCSVをBULK INSERTするコードを記述
  3. 各modelにオプションを設定
  4. 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.rb

2. 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'))

end

3. 各modelにオプションを設定

app/models/hoge.rb
class 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
end
app/models/fuga.rb
class 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
end

4. rails db:seed(完) ?

bash
rails db:seed

関連URL

所感

久しぶりに書いた。業務でCSVインポートの処理を実装するの何気に多い。
後で再インプットも兼ねて解説コメント丁寧に書く。
気力があれば過去記事のメンテナンスしていく

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

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.rb
FactoryBot.define do
  factory :user do
    first_name { '太郎' }
    last_name { '山田' }
    email {'testuser@example.com'}
  end
end

このようにFactoryBotでUserを作り、そのModelのテストを行うとします。

user_spec.rb
describe '#full_name' do
  let(:user) { create(:user) }
  it '山田太郎を返す' do
    expect(user.full_name).to eq '山田太郎'
  end
end

ところがこれでは正常に動作しないです。
letが評価されるタイミングはuserを呼び出した時です。なのでfull_name実行時にはuserは作られておらずうまくいかないのです。

user_spec.rb
describe '#full_name' do
  let!(:user) { create(:user) }
  it '山田太郎を返す' do
    expect(user.full_name).to eq '山田太郎'
  end
end

letの後ろに!をつけることで、itの中身が実行される前に中身が作られるようにするとうまくいきます。

その2 サブドメインのテスト

こんな感じのルートがあった時に、サブドメイン配下のRequest specを書きたい場合はどうすればいいかハマりました。

routes.rb
constraints subdomain: 'hoge' do
  get '/' => subdomain#index
end

正解はこちら⬇︎

subdomain_request_spec.rb
describe '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.rb
describe '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.rb
describe '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
end

setupブロックで事前に/loginにpostしておくことでセッションを生成しておくことができます。
そのあとにexampleを書いていけばOK!

その5 CarrierWaveを使っているところのテスト

Carrierwaveを使っていて画像をアップロードする部分のテストはどうすればいいの!

upload_request_spec.rb
  before(: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さんの登場です。お楽しみに。

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

ド素人が超簡単な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

おわりに

言語やフレームワークにこだわらないで色々面接受けていこうと思う。
未経験者の戦闘力がインフレしているらしいので不安だけども。

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

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    #=>0

0xは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クラス

とりあえず、そのへんを固めていきたいです。

以上になります。

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

これから学ぶ言語についてのまとめ

これから学ぶ言語について

これからプログラミングを学ぶにあたり、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アプリケーションを簡単に開発できるようにする骨組みのこと。

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

配列とハッシュ

配列とハッシュ

日々のRubyの学習で学んだことを備忘録としてつぶやいていきます。

配列

配列とは?

通常の変数を1つの箱と捉えれば、配列は、仕切りがある箱のようなもの。
配列を用いることで1つの変数で複数の値を管理することができる。
配列は、要素を順番で管理する。
◯番目といった順番を表す数字のことをインデックスと言う。

インデックスは0番目から始めることに注意!!

配列の書き方

ary.rb
ary = ["10","20","30"]
# 3つの値を1つの変数に格納している

ハッシュ

ハッシュとは?

ハッシュも配列と同じで、複数の値を一括で管理したいときに使われる。
配列は、順番で管理していたことに対し、ハッシュは、名前で管理することができる。
ハッシュとは、キー名: 値の形式で値を取ってくることができる。

ハッシュの書き方

hash.rb
hash = {name: "reo", age: 20, country: "japan"}
# キー名: バリューの形式で値を格納している

配列のハッシュの知識を組み合わせたコード

work.rb
students = [
  {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点
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.3

Stackdriverに切り替える

これまでの構成

  • 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.rb
config.log_formatter = eval Settings.logger.formatter
Log
severity:[ 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とは

スイートに含まれるパッケージ・ライブラリ

Stackdriver Debugger
  • コードの動作を調査し、デバッグを行える
  • gem を組み込んだら、Cloud Console に移動し、コードをアップロードします(または、Stackdriver Debugger でリポジトリ内のコードを指定します)。すると、実行中のアプリケーションの変数値とスタックトレースを含むスナップショットを取得
  • 実行中のアプリケーションに任意のログ文を追加することも可能
Stackdriver Error Reporting
  • 例外を検出して報告する Google Cloud ツール
  • アプリケーションのクラッシュを捉え、それを論理的にグループ化して通知してくれる
    • UI(エラー管理インターフェース)に整理してわかりやすく表示
    • UI には、エラーのスタックトレースや、各クラッシュのログおよび分散トレースへのリンクが表示される
    • グループ化されたバグ データベースから発生したバグを関連づけることができる
Stackdriver Trace
  • アプリケーションのパフォーマンス モニタリングと分散トレーシングを行う
  • リクエストがデータベースに到達し、ビューがレンダリングされ、アプリケーション ロジックの実行に要した時間が自動的に表示される
  • リクエストがマイクロサービス アーキテクチャでどのように処理されるかを表示
    • レイテンシの時系列の傾向に関する詳細レポートを提供
    • 最新リリースへの更新後、アプリケーションに遅延はないかチェックできる
Stackdriver Logging

導入手順

  1. GCPプロジェクトの請求先設定がされていることを確認※設定されていないと使用できない模様

2. GCPプロジェクト上にStackdriver Logging APIをインストールする

3. Gemを追加する

Terminal
vi Gemfile
Gemfile
# 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"
end

6. 起動確認

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

使い方は追って記載します

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

俺たちは雰囲気でAdminLTEを使っている

はじめに

※この記事は、「RUNTEQ Advent Calendar 2019」、19日目の記事です。

普段、Webサービス・アプリのUIデザインを仕事としてやっているのですが、管理画面の制作は、エンジニアの誰かが良しなに対応してくれているだけという状況がかなり多いです。

社内のRailsエンジニアと話していたときに、「AdminLTEなんかもっといい感じに使えたいんだよね」というお悩み相談を受けたことがあります。
今回は、AdminLTEを使用する際に持っていると助かりそうな知識と、ちょっとしたテクニックをまとめてみました。

デザイナーがいないプロジェクトでも、CSSフレームワーク等を用いてせっせと管理画面をつくってくれている、最高のエンジニアの方々のお役に立てたら最高です。

AdminLTEとはなにか

AdminLTEは、Bootstrapベースでつくられた、管理画面作成などに特化したCSSフレームワークです。
これから使うならバージョン3以降がおすすめです。

骨組みはBootstrapで、それにスタイリングを加えて、サンプルも提供してくれているものがAdminLTEと考えてもらえると良いかなと思います。

スクリーンショット 2019-12-09 15.06.27.png (513.9 kB)

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

プロジェクトでよくみかけるのは、ボーダーつきのテーブルです。

スクリーンショット 2019-12-09 15.19.16.png (87.6 kB)

.card
  .card-header
    .card-title
    .card-tools
  .card-body
    table.table.table-bordered
      thead
      tbody
  .card-footer

ただ、このままだとレスポンシブ対応してません。
そんなときは.card-body.table-responsiveをつけるといい感じになるかと思いきや、上部の線がいなくなってしまいます。

スクリーンショット 2019-12-14 13.53.37.png (27.7 kB)

.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

良い感じのテーブルができました。

スクリーンショット 2019-12-14 13.56.57.png (27.7 kB)

これを鬼のようにコピペすると、管理画面はほぼ完成です。

あとはちょっとしたテクニックですが、数値を表示するときは、小数点以下の桁数を揃えて.text-rightをつけると見やすくなります。

スクリーンショット 2019-12-19 19.53.39.png

table.table.table-bordered.border-top
  thead
    tr
      th.text-right
  tbody
    tr
      td.text-right

導線のつくりかたを統一する

いったんCRUDどおりにやって悩まないのが良いと思います。

サイドメニューに、ユーザー管理という区分を設けて、一覧・作成を配置します。

スクリーンショット 2019-12-09 15.50.41.png (22.4 kB)

コンテンツ部分はサンプル通りに、タイトル、パンくず、コンテンツを並べて、このレイアウトを使い回すつもりでやりましょう。

スクリーンショット 2019-12-20 10.18.48.png

基本の骨組みを省こうとすると、別の調整が必要になるので、いったんは教科書どおりに使うべきです。

そのページでやれることを絞る

一覧ページじゃなくて、詳細ページにだけあればいいものをトルという方針でやるといいです。
一覧ページでは詳細・編集ボタン、詳細ページでは編集・複製・削除ボタンのように区別をするといい感じです。

さらに、IDやタイトル名にリンクを貼ると、詳細ボタンを省略できて、アクション部分がスッキリします。

スクリーンショット 2019-12-19 19.10.37.png

※あくまでも指針なので、複製・削除をすることがものすごく多いのであれば、一覧ページにも置くべきではあります

項目のグループをつくる

例えば、ユーザー新規登録ページがあったとします。単純に項目を並べると、こんな感じになります。

スクリーンショット 2019-12-14 14.33.30.png (67.2 kB)

入力項目などが多数ある場合は、似たものを近づけてグループ化してあげると、心理的な負担が和らぎます。

そのグループごとに、見出しをつけるといい感じです。ページ自体は長くなってしまうのですが、小さいゴールがあると、ツラさが少し和らぎます。

スクリーンショット 2019-12-19 19.14.27.png

ページが長くなるのがどうしてもツラい場合は、レイアウトを変えると、多少短くはできます。

スクリーンショット 2019-12-19 19.16.33.png

もっと使いやすくするなら、入力エリアは入力量に近い見た目にするといいです。ゴールや負荷が見えたほうが、タスクには取り組みやすくなります。

スクリーンショット 2019-12-19 19.18.28.png

上書き用の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つです。

①近接→関連があるものは近づける、関連がないものは余白をつける
②整列→端をそろえる
③反復→繰り返す
④対比(コントラスト)→違うものには差をつける

きちんと知りたい方は、ノンデザイナーズデザインブックという書籍を読むと最高です。

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

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でエスケープを解除していますね

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

エンティティについて

エンティティ

エンティティとは

  • エンティティとは、共通項を持ったデータの集合体
    • データの対象を抽象的に捉える。
    • 属性とインスタンスを持っている。
    • 簡単に言うとER図の箱のこと
    • サービスで扱う情報のこと

(例)車をWeb上で販売するアプリ

  物理的なエンティティ 社員・店舗・車、顧客など
  概念的なエンティティ 購入履歴・進捗状況など

image.png

エンティティの属性

エンティティの属性とはエンティティが持つ個別の情報のことをいう
車のエンティティの属性は価格、色、大きさ
顧客のエンティティの属性は 名前、年齢、性別

memo

  • エンティティはDB設計する上で大切な情報。
  • DB設計するときは、粒度の大きなエンティティから作成していくといい。

参考

https://wa3.i-3-i.info/word11594.htmlv

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

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に保存すると管理がしずらくなったりするのでそれを解消したりできる。

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

Rubyのお型付け!Sorbetを試してみた

Ateam cyma Advent Calendar 2019、20日目です!いよいよ終盤ですね!
本日は 株式会社エイチーム のねぼすけエンジニア @NamedPython がお送りします!

さて、タイトルで「お?Rubyに型?」と思ったあなた!そう、ついにRubyにも型の時代がやってこようとしているのです!
今日は、RubyKaigi2019にてStripe社から発表があったオープンソースのRuby向け型チェッカーであるSorbet?をふんわり紹介します!

Sorbet logo

どういう経緯・思想で作られたかは、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: ignore typed: false typed: true typed: strict typed: strong
解析をしないし、エラーを出さない 解析し、アノテーションがある部分のみエラーを出す 解析し、型を認識できない場合はエラーを出す エラーを出す上に、型アノテーションを強制する すべての型チェックエラーを出す

typed: ignoretyped: 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ではないそうです(すげえ:rolling_eyes:)

# 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: true
  • sigによるアノテーション

ができています。

この状態で型チェックをやってみます # => bundle exec srb tc
image.png
おー、型チェック問題なしなんですね。

それならこの(危険な)メソッドが外部からアノテーションと違う呼び出され方、返し方をしたとしましょう。
例えばこんなコード:

unsafe_code.rb
# frozen_string_literal: true
# typed: false

require_relative 'sample'

result = Sample.typed_nillable_method("a") # 文字列を指定

result.split('').join('-')

おやおや、SymbolとアノテーションしているのにStringを渡していますね。この状態で型チェックを行うと....?
image.png
すごーーーい!
Expected Symbol but found String("a") for argument key https://srb.help/7002 ですって。具体的で親切なメッセージですね。

今は手動で実行していますが、GitLab CIVSCode 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('-')

懸念していた危険なケース、存在しないキーを参照しているようです。型チェックは....?
image.png
通っちゃいますね...。でも実行すると....?
image.png

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をご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

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

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使って書いてみると色々と気づくものもありますが今の時代なんでもそろってるからそっちを使った方が楽なのは間違いないですね?

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

○witterもどきのアプリでハッシュタグ機能を実装してみた。

はじめに

どうも、Pirikaraです。
今回は適当に作った○witterもどきのアプリで、ハッシュタグ機能を実装してみたので共有したいと思います。

まずはrails new

まずはT○itterもどきのアプリを作ります。
tag機能を実装するので、rails newでtag_appを作ります(適当)

tweetモデルとtagモデルを作成しますが、
両者は多対多の関係なので、中間テーブルとしてtweet_tagモデルも作っておきます。

tweet.rb
class Tweet < ApplicationRecord
  has_many :tweet_tags
  has_many :tags, through: :tweet_tags
  #写真の投稿も可能にするために、carrierwaveとminimagickを入れておいた
  mount_uploader :image, ImageUploader
end
tag.rb
class Tag < ApplicationRecord
  has_many :tweet_tags
  has_many :tweets, through: :tweet_tags
end
tweet_tag.rb
class TweetTag < ApplicationRecord
  belongs_to :tweet
  belongs_to :tag
end

まずはImageとTextのみ投稿ができるTw○tterもどきアプリを作りました。

Image from Gyazo
コードはこちら。

tweets_controller.rb
class 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テーブルに保存される形でコードを書いていきたいと思います。

仕様と進め方の確認

  1. textareaに『#(ハッシュタグ)』があれば『#』以下をtagとして認識し、Tagsテーブルに保存する
  2. 一覧表示画面にて、tag名が『#○○○』の形で表示される
  3. 新規投稿画面にて、Tagsのプルダウンにこれまで登録されたTagが表示される。

今回はtweets_controllerのcreateアクションでTweet.createされたあと、
model/tweet.rbでActiveRecordの『after_create』というコールバックを使用し、データベースに保存される直前にTagsテーブルに『#』タグ以下を登録させる感じでやっていきます。
after_create等のコールバックについては、下記のサイトを参考にしました。

Railsガイド

それではいざ、実装。

Tweet Modelにコールバックの記述

after_create do 〜 endの間に処理を記述していきます。
1. controller側でcreateしたTweetを取得
2. Tweetのtextカラムの値から、『#〇〇〇』に該当する箇所を検出
3. tagsテーブルに保存

加えて、view側で『#〇〇〇』をtagsテーブル経由で表示させまます。

4.『#〇〇〇』を含む形でtweetsテーブルに保存されているので、viewで出力する際に正規表現を使って取り除く。

コードは以下の通りです。

models/tweet.rb
class 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
end
tweets/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を表示させることができました。
Image from Gyazo
DBにもちゃんと保存されていますね。
めでたしめでたし。
スクリーンショット 2019-12-19 15.27.44.png
スクリーンショット 2019-12-19 15.27.58.png
スクリーンショット 2019-12-19 15.28.15.png

おわりに

今回はTwitte○もどきのアプリでハッシュタグ機能の実装にチャレンジしてみました。
View側で表示させる時に正規表現を使っているところが不恰好なのでヘルパーに突っ込んでもいいかもしれませんね。

応用としては、ハッシュタグからリンクを飛ばして紐づいたtweetを一覧表示できる・・・・・・とかですかね。
ぜひチャレンジしてみてください。

おわり。

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

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.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;
}

細かく見ていきます。まずは 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.rb
class 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.rb
  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

指定された 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.rb
  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

show アクションとの違いは特にないのですが、データの詰め替えの部分の処理が長ったらしくなってしまっています... この辺は要リファクタリングです。

create

最後に、 create アクションです。

app/controllers/proto/tasks_controller.rb
  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

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.0

protoc のインストール

proto ファイルから実装コードを自動生成する際に、 protoc というライブラリが必要になるのでインストールします。

$ brew install protobuf

protobuf 用の 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:string

validation の追加

app/models/task.rb
class Task < ApplicationRecord
  validates :title, presence: true, length: { in: 1..25 }
  validates :description, length: { maximum: 100 }
end

各種制約の追加

db/migrate/20190000000000_create_tasks.rb
class 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
end

migration を実行しましょう。

$ bundle exec rails db:migrate

続いて、開発がしやすいように seed データを準備します。

db/seeds.rb
Task.create(title: '大掃除', description: '年末なので気だるいけど大掃除をする')
Task.create(title: 'advent calendar を書く', description: 'そろそろ書き始めないとヤバイ')

忘れずデータを投入しておきましょう。

$ bundle exec rails db:seed

proto ファイルでの スキーマの定義

次に、システム間でどういうデータ形式でデータをやりとりするのかを、 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
end

lib 配下に 自動生成されるファイルを置くようにしたので、下記を追記してちゃんと読み込まれるようにします。

config/application.rb
Dir["#{Rails.root}/lib/proto/*.rb"].each { |file| require file }

route.rb で API のエンドポイントを定義

route.rb
Rails.application.routes.draw do
  namespace :proto do
    resources :tasks
  end
end

controller の実装

app/controllers/proto/tasks_controller.rb
class 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 を見てみると、文字化けしたものが出力されていると思います。(自分の環境ではゴミデータも混ざっているので、写真の通りには出てこないと思いますが悪しからず)

image.png

テストの作成

長くなりそうだったので、 index アクションのテストだけを記載しました。他のアクションのテストについては、こちらのリポジトリをご確認ください。

spec/requests/proto/tasks_spec.rb
require '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
end

index, show メソッドをリファクタリング

index アクションにコードが長々とあるのでリファクタリングをします。

app/controllers/proto/tasks_controller.rb
  def 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
  end
app/models/task.rb
class 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の公式の方に google という prefix がわざわざついています。ややこしい。)
後者の gem も今でもメンテナンスされており、使用しても問題ないように思います。
(なぜ複数の active な gem が存在しているのかということはちゃんとは調べていないのですが、公式の protobuf の ruby ライブラリ(gem)より先に、有志の gem が作られたのかなー?と勝手に予想しています。現に proto2 の時点では ruby で protobuf を扱うことができなかったようです(現在は proto3 がデフォルトなので1つ前のバージョンの時)。)

どちらの gem を選択するかは難しいところですが、自分が知っている限りでのそれぞれの特徴を書いておこうと思います。

google-protobuf

リポジトリ: google-protobuf

  • google が公開しているものなので、ちゃんとメンテナンスされ続けそう。
  • gem の実装に Java とか C などのコードが使われている。
  • 自動生成される ruby のコードがわかりにくい。

protobuf

リポジトリ: protobuf

まとめ

個人で趣味として使うのであれば protobuf の方が、使いやすそうだなーと個人的に思っています。 protobuf-activerecord を使って、実装をシンプルにできそうだし。業務で使う場合にどちらを使用するかは、皆さんでご判断ください。

付録3 例外処理

protobuf で自動生成されたコードを使って実装を進める場合、レスポンスは proto ファイルで定義した response の型で返してあげる必要があります。そのため、 ApplicationController に全メソッド共通の例外処理などを簡単には書くことはできません。そのため、個人的には下記のような形式が まだ 綺麗な方じゃないかなーと思っています。

app/controllers/proto/tasks_controller.rb
  def 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

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

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.rb
GC::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の組み込みモジュールのGCGC:: 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_objects6333071個にまで増えているのを見ると、生成した変数a内のオブジェクトのほとんどが古いオブジェクトとしてマークされているのが分かりました。

手動でマイナーGCを実行してみる

次にGC.startを使って手動でマイナーGCを実行してみます。

hoge.rb
GC::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.rb
GC::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_slots6326296まで増えていますね。

最後に

ざっくりとですが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

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

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 ~/.rbenv

2-2.環境設定 PATHに追加する

echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc

2-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.3

rubyの構築が完了した

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]などのバージョンが表示されればよい。

(参考にさせていただいたサイト)

https://mhaya18.hatenablog.com/entry/2018/09/01/090051

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

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 が無いと,スクリプトは一瞬で終わってしまう。


  1. コマンド名が思い出せなければ gem spec slim で slim gem の情報を表示し,executables のところを見れば slimrb と出ている。 

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

自動メール送信作ってみた

アドベントカレンダークリスマスイブ担当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使って書いてみると色々と気づくものもありますが今の時代なんでもそろってるからそっちを使った方が楽なのは間違いないですね?

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

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Capistranoで自動デプロイ時に起きるエラーについて〜AWS〜

はじめに

Capistranoを使って、AWSに自動デプロイをかけた時に出るエラーのお話
本件は主に再起動時に起きるエラーです
最後にデプロイ時に確認する事項をまとめていますので、そちらも軽くご参照いただければと思います

Erroe内容

bundle exec cap production deploy実行後

terminal
Mac_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.pem

hoge.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.

上記ができているにも関わらずデプロイできないときは、インスタンスを一度停止しましょう
スクリーンショット 2019-12-19 11.36.51.png
"停止"が完了したら"開始"をしましょう
大概のエラーはこれでで解決できます。

まとめ

解決方法のまとめです

  • "Net::SSH::AuthenticationFailed: Authentication failed"が出たときはsshキーが消えている可能性があるので、addしてやる
  • mysql/nginexの再起動
  • unicornのkill
  • インスタンスの再起動
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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について で確認できます
スクリーンショット 2019-12-19 11.06.28.png

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

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

[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について で確認できます
スクリーンショット 2019-12-19 11.06.28.png

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

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

.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?
=>false

empty?

.emptyは、オブジェクトの長さ== 0を意味します。このメソッドは、文字列、配列、ハッシュで使用できます。

"".empty?
=> true

ただし、空白の場合、改行またはタブ.emptyはfalseを返します。 空白、改行またはタブも文字としてカウントされます。

" ".empty?
=> false
"\t\n".empty?
=> false

Array ,hashes, set の場合、要素がない場合はtrueを返します。

[].empty?
=> true
{}.empty?
=> true

blank?

.blank? ActiveSupportのRails method です。
これは、nil、false、empty、empty stringに対してtrueを返します。

nil.blank?
=> true
false.blank?
=> true
"".blank?
=> true
" ".blank?
=> true 
[].blank?
=> true
{}.blank?
=> true

present?

.present?も Rails method です。 .blank?の反対です.

[].present?
=> false
" ".present?
=> false
false.present?
=> false
[1, 2, 3, 4, 5].present?
=> true

これがお役に立てば幸いです :)

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

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つの方法があります。
  1. gemをglobal installする
  2. Gemfileに追加し、bundlerでinstallする
  3. docker containerなど外部サーバーとして動かす

1. gemをglobal installする場合

solargraphをインストールします。

$ gem install solargraph

# グローバルなgemのdocumentを生成します。
$ yard gems

settings.jsonは以下のように設定しましょう。

"solargraph.useBundler": false

2. bundlerでinstallする場合

Gemfileにsolargraphを追加してインストールします。

gem 'solargraph', group: :development
$ bundle install

# プロジェクトにインストールしているgemのdocumentを生成します
$ bundle exec yard gems

settings.jsonは以下のように設定しましょう。

"solargraph.useBundler": true

1と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.yml
require:
- actioncable
- actionmailer
- actionpack
- actionview
- activejob
- activemodel
- activerecord
- activestorage
- activesupport

3. 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を自動挿入してくれます。
end挿入例

Rainbow End
endを色分けする拡張機能です。
do~endがネストする時に見分けにくかったので、入れています。
画面例

その他Ruby・Railsに限らない拡張機能

indent-rainbow
インデントに応じて色分けしてくれます。
色分け例

Material Icon Theme
エクスプローラー内のフォルダやファイルアイコンなどが見やすくなるテーマです。
スクリーンショット 2019-12-18 19.52.51.png

Swagger Viewer
VSCode上でSwaggerファイルのプレビューが可能になります。

最後に

以上、拡張機能とその設定でした。
ぜひ気になったものがあったら使ってみてください。

明日は @yosuke_ohara さんです。お楽しみに!

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

画像の複数投稿??プレビュー表示??え??

〇〇エキスパートにて

どうも、pirikaraです。
チーム開発にて某メルカリのクローンサイトを作成中、商品出品の画像投稿で詰まりました。
プレビューは表示されるのにデータが入ってない・・・・・・
プレビュー消したのにデータが残っている・・・・・・
など散々格闘したので、参考になればと思って書きました。

10月からプログラミング学習を開始した弱々エンジニアですので、お手柔らかによろしくお願い致します。

こんなやつ作ります(個人アプリで作ったやつです)
21jfi-uf4mh.gif

仕様

  1. 画像を10枚投稿できる
  2. 投稿した画像は1枚ずつプレビューされる
  3. 5枚目以降は2段目にプレビュー表示される
  4. 削除を押すとプレビューから消える
  5. 追加・削除したプレビューとfile_fieldの中身が同期している

やってみよう

まずはrails newで適当なアプリケーションを立ち上げます。
今回は適当にsample_appとしました。
databaseはmySQLを使用します。

今回は商品出品なので、ItemモデルとImageモデルを作成。
CarrierWaveとminimagickのgemをインストールして、
アソシエーションを組みます。

item.rb
class Item < ApplicationRecord
  has_many :images, dependent: :destroy
  accepts_nested_attributes_for :images, allow_destroy: true
end
image.rb
class 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カラムに画像ファイル名が保存されるようにしました。
スクリーンショット 2019-12-18 15.19.54.png
スクリーンショット 2019-12-18 15.19.34.png

フォームを作るよ

適当にフォームを作っていきます。
スクリーンショット 2019-12-18 16.05.23.png

できました。

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. プレビューの表示

できました。

Image from Gyazo

コードは以下の通りです。

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.js
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>`

削除ボタンを押すと発火するイベントを作成し、プレビューが削除できるようにします。

new_item.js
$(document).on("click", '.item-image__operetion--delete', function(){
  //プレビュー要素を取得
  var target_image = $(this).parent().parent()
  //プレビューを削除
  target_image.remove();
  //inputタグに入ったファイルを削除
  file_field.val("")
})

Image from Gyazo

file_fieldに画像が入ったと同時にプレビューが出て、プレビュー削除と同時にfile_fieldに入った画像が消えました。

3. プレビューの複数表示

ここからが本番です。
大まかな流れとしては、
①DataTransferオブジェクトを作成し、データを格納する箱とする
②each文を用いて、DataTransferオブジェクトの箱にfileを追加していく。
③箱の中身をfile_fieldに代入して、複数データを持たせる
④ついでeach文の中でプレビューを追加していく。
って感じです。

DataTransferオブジェクトに関しては、下記のサイトを参考にしました。
Javascriptプログラミング講座

スクリーンショット 2019-12-19 0.57.04.png

画像を挿入すると、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__container
item_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;
  }
}

できました。
Image from Gyazo
プレビューの追加に応じて、「ファイルを選択」欄のファイル数が増えていくようになりました。

しかしこのままでは以下のコードによって、削除ボタンを押すと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}`)
})

できました。
Image from Gyazo

ドラッグ&ドロップへの対応

ドロップエリアをクリックした時にファイルが入る&プレビューが表示される実装が完了したので、ドラッグ&ドロップでも画像が投稿できるようにコードを追加します。
ドラッグ時にドロップエリアに影がつくようにしてみました。
プレビュー表示、file_fieldへのfile追加に関してはクリックによるものとほとんど同じです。

new_item.js
var 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}`)
    })
  })
}

できました。
Image from Gyazo

databaseにも無事保存されましたね。めでたしめでたし。
スクリーンショット 2019-12-19 2.00.12.png

終わりに

上記のコードを参考にして、ご自身のアプリで挙動を確かめてみてください。
無事に動くと良いですね。

問題点としては
『同じ画像を貼り付けた場合に、削除を押すと複数同時に消えてしまう』
というエラーを抱えていることでしょうか・・・・・・(大問題)

直したら追記します。
直せる強強エンジニアのかたいらっしゃいましたら教えてください。

以上、初投稿でした。
お目通しいただきありがとうございました。

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