20191219のRailsに関する記事は22件です。

[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で続きを読む

RailsでJSON REST APIを実装する(GET編)

背景

現在、RailsのapiモードでJSON APIサーバを開発している。
RESTful APIの原則に従い、各種リソースに対するGETPUTPOSTDELETEを実装している。

今回はGETの実装について書きます。

やりたいこと

クエリパラメータでリソースの属性を指定し、条件に合うリソースを全てGETするJSON APIを実装したい。

GET /staffs?store_id=12345678&gender=male&grade=professional

クエリパラメータには store_idgendergrade を設定できる。

全てのパラメータを記述しなくてもよく、任意のパラメータだけでリソースを絞ることを可能にしたい。

少し悩んだ点

例えば、

GET /staffs?store_id=12345678

というリクエストが来た場合、 params[:gender]params[:grade]nilになってしまうため、モデルの検索条件から省いてやらなければならない。

if文で条件分けするのではあまりに芸がない。

実装

reduceを使ってnilとなるパラメータを除外し、検索条件を作る方法で解決した。

  def list
    render json: Staff.where(
      [ 
        { room_id: params[:room_id] },
        { gender: params[:gender] },
        { grade: params[:grade] }
      ].reduce({}) do |pre, cur|
        cur.values.first.present? ? pre.merge(cur) : pre
      end
    ), status: 200
  end

終わり。

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

【Rails5.2.4】rails newしたらsass-railsとsprocketsのバージョンが合わなくてエラー発生

はじめに

Rails+Vue.jsの環境構築をしていたときに発生した単純なエラーについて解決法を残します。

環境

OS: macOS Catalina 10.15.1
zsh: 5.7.1
Ruby: 2.6.5
Rails: 5.2.4.1

エラー内容

$ rails new
...略
Resolving dependencies....
Bundler could not find compatible versions for gem "sprockets":
  In snapshot (Gemfile.lock):
    sprockets (= 4.0.0)

  In Gemfile:
    sass-rails (~> 5.0) was resolved to 5.1.0, which depends on
      sprockets (>= 2.8, < 4.0)

    rails (~> 5.2.4, >= 5.2.4.1) was resolved to 5.2.4.1, which depends on
      sprockets-rails (>= 2.0.0) was resolved to 3.2.1, which depends on
        sprockets (>= 3.0.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
       rails  webpacker:install
Could not find gem 'mysql2 (>= 0.4.4, < 0.6.0)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.
...略

sprocketsのバージョンが4.0.0だけど、
sass-rails(~> 5.0)sprockets(>= 2.8, < 4.0)に対応してるからうまくいかないよ!という内容。

解決法

Gemfile
gem 'sass-rails', '~> 5.0'

Gemfile
gem 'sass-rails'

$ bundle install

バージョン指定を外してあげれば無事通るようになりました。

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかのエラー解決時間の短縮につながればと思います。:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

【Rails】Vue.js入門してみた。(rails new ~ hello?) - おぴよの気まぐれ日記

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

「rails server」コマンドを読み解く

「rails server」コマンドを読み解く

前置き

本記事の内容はタイトルのまんま。rails serverコマンドを叩いた際に一体何が実行されているのかを調べてみた。内容的に、対象者は初心者以上中級者未満。多分。なお、調べながら記事を書いていたので、説明の流れが下手くそな部分があるがそこは許してンゴンゴ。

※Rackについて詳しい人は得るものないかもしれませんがご了承ください。

バージョン

ruby 2.6.3
Rails 6.0.1

準備

多分そんなに必要ないけどRails newしてアプリを作成。

$ rails new adc2019

からのbundle install

$ bundle install

コントローラも作成してルートを通しておこう。

rails g controller home
routes.rb
Rails.application.routes.draw do
  get '/', to: 'home#homw'
end

viewsは適当で

home.html.erb
<h1>HOME</h1>

んでrails s

$ rails s

localhost:3000にアクセス

スクリーンショット 2019-12-13 18.48.26.png

rails server とは

早速、rails serverの中身をみていく。

rails コマンド

基本、ターミナル上でrailsコマンドを叩いた場合、システム上のrailsコマンドを呼ぶ。

$ which rails
/Users/かもひこ/.rbenv/shims/rails

んでもって、`rails newして作ったアプリのディレクトリにおいては、その配下のbin/railsが呼ばれる。
(上記システム上のrailsを呼ぶ。→からのアプリディレクトリ配下のbin/railsを呼ぶ。という流れ。)

中身は下記のとおり、

bin/rails.rb
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

最初の部分でspring(アプリケーションプリローダー)を呼び出しとる。
コマンドの2回目以降の起動時間を短縮してくれるやつ。
Rails4.1からデフォルトで追加されたらしい。

次にAPP_PATHを定義して、なにやらconfig系のファイルを読み、やっと最後rails/commandsという、如何にもなファイルを読みにいく。
rails serverが何をやっているのかを知りたいので、configの部分は「なんか初期化やら設定してるんやろな」ぐらいに済ませて、さっさとrails/commandsを見てみる。(ここからはgemのrailsをみていく。アプリ配下のファイルではないのでご注意を。)

commands.rb

rails/railties/lib/rails/commands.rb
require "rails/command"

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

Rails::Command.invoke command, ARGV

ここでコマンドのエイリアスを定義してる。普段rails sとかでrails server扱いしてくれているのはここのおかげ。

んで、最後のinvokeメソッドの記述部分。コマンドラインで叩いたserverinvokeに渡している。直略で呼び出すの意味を持つこのメソッド。
次に見るんはここやな。

てわけでinvokeメソッドを見てみる。

command.rb

rails/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end

namespaceって書いてあるあたり、名前空間が引数(今回でいうとserver)のモジュールを読みにいくのかな?

下記の部分がおそらくそうっぽい。

command = find_by_namespace(namespace, command_name)

find_by_namespaceは下記のとおり。

rails/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

最初の3行で探しにいくモジュールの名前空間を配列にぶちこんでいる。lookupsの中身は下記。
["server", "server:server", "rails:server", "rails:server:server"]
rails:serverだけでよくね?と思ったが、きっと何か理由があるのだろう。(他のコマンド使用時に必要な処理かな?)とりあえず進む。

lookupメソッドに上記の名前空間たちを渡している。きっとこのメソッド内でモジュールを読み込むのだろう。

rails/railties/lib/rails/command/behavior.rb
def lookup(namespaces)
  paths = namespaces_to_paths(namespaces)

  paths.each do |raw_path|
    lookup_paths.each do |base|
      path = "#{base}/#{raw_path}_#{command_type}"

      begin
        require path
        return
      rescue LoadError => e
        raise unless /#{Regexp.escape(path)}$/.match?(e.message)
      rescue Exception => e
        warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
      end
    end
  end
end

場所は変わってbehavior.rb内にlookupメソッドはいる。
最初の一行目で渡されてきた名前空間たちをパスへと変換している。
んで、案の定requireしている。そろそろメソッド一つ一つ追っていくのがだるくなったのでpathputsして確認する。
(できればソースを直にいじるべきではないが、パッチをあてるのもあれだったので。)

begin
  puts path #ここで確認
  require path
  return

んでサーバーたてる。

$ rails server
rails/commands/server/server_command
=> Booting Puma
=> Rails 6.0.1 application starting in development 
...
..
.
〜略〜

でましたでました。ここでやっと、

requireしたのはrails/commands/server/server_commandか!!じゃあ、serverコマンド叩いたときの処理はそこを見ればいいんだな!」(棒読み)

となるわけである。(まあソース開いてディレクトリ構成見た時点で予想はついてたけど、我慢して順繰りに処理を追ってみた。)

さて、モジュールを読み込んだところで、少し戻ってfind_by_namespaceの下記の部分。

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]

モジュールを読み込んだだけでは意味がない。最後に返り値としてサブクラスを返している。
つまりRails::Command::ServerCommandを返す。

そして、ここでinvokeメソッドに戻る。下記の部分である。

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end

commandの中には上記サブクラスが入っており、その中にあるメソッドperformを実行している。
ようやくここでserverコマンドの処理が実行されるわけである。

てわけで、いよいよserver_command.rbの中をみる。

server_command.rb

ではperformメソッドをみてみる。

rails/railties/lib/rails/commands/server/server_command.rb
def perform
  extract_environment_option_from_argument
  set_application_directory!
  prepare_restart

  Rails::Server.new(server_options).tap do |server|
    # Require application after server sets environment to propagate
    # the --environment option.
    require APP_PATH
    Dir.chdir(Rails.application.root)

    if server.serveable?
      print_boot_information(server.server, server.served_url)
      after_stop_callback = -> { say "Exiting" unless options[:daemon] }
      server.start(after_stop_callback)
    else
      say rack_server_suggestion(using)
    end
  end
end

まだまだコードリーディングが続くのかあ....と思っていたが、じつはここの部分、公式ガイドラインにて説明されている。(→ここから

Rails::ServerクラスはRack::Serverクラスを継承しており、以下のようにRack::Server#startを呼び出します。

つまり上記のまんま、rails serverはRackよりサーバーを起動していたのである。

Rackとは

rails sがRackよりサーバーを起動してるとわかったところで、ここでRackについて説明する。

Rackとは

Rackについて説明しているページより下記を引用する。

In the words of the author of Rack – Christian Neukirchen: Rack aims to provide a minimal API for connecting web servers supporting Ruby (like WEBrick, Mongrel etc.) and Ruby web frameworks (like Rails, Sinatra etc.).

簡潔に説明すると、RackとはRuby製のWEBサーバーとWEBアプリケーション間のインターフェースである。様々なwebサーバー、WEBアプリケーションが双方依存せずに開発できるのはこのRackのおかげ。

Rackの仕様に従って開発されているWEBサーバー、WEBアプリケーションであればどのような組み合わせも可能なのである。(puma & rails、 unicorn & rails, unicorn & sinatra みたいな)

Rackを使ってみる1

それでは実際に、引用元のページを参考にしてRackの仕様に従ったアプリ、いわゆるRackアプリケーションを作ってみる。Rackアプリケーションとしての最低限の仕様は下記のとおり。

A Rack application is a Ruby object that has a call method, which has a single argument, the environment, (corresponding to a HTTP request) and returns an array of 3 elements, status, headers and body (corresponding to a HTTP response).

つまり、

  1. callメソッドを持つこと。
  2. 引数としてWEBサーバーからのHTTPリクエストを受けること
  3. 返り値は、ステータスコード(100以上)、レスポンスヘッダ(ハッシュ)、レスポンスボディであること。

の3つがRackアプリケーションとしての最低限の条件である。

それでirbで簡単に作ってみる。(ここは参考元のほぼ丸パクリ。)

$ irb

まず、rackを読み込む。

irb(main):001:0> require 'rack'
=> true

callメソッドを持っていればよいので、lambdaを使ってProcオブジェクトを作る。
引数は一つ。返り値は仕様どおりのレスポンスコード、レスポンスヘッダ、レスポンスボディ。

irb(main):002:0> my_app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello. The time is #{Time.now}"]] }
=> #<Proc:0x00007fdb888ff528@(irb):2 (lambda)>

試しに実行。

irb(main):003:0> my_app.call({})
=> [200, {"Content-Type"=>"text/plain"}, ["Hello. The time is 2019-12-19 11:26:39 +0900"]]

問題ないので、最後にWEBrickをWEBサーバーとしてをアプリ起動、

irb(main):004:0> Rack::Handler::WEBrick.run my_app, :Port => 9876
[2019-12-19 11:27:02] INFO  WEBrick 1.4.2
[2019-12-19 11:27:02] INFO  ruby 2.6.3 (2019-04-16) [x86_64-darwin16]
[2019-12-19 11:27:02] INFO  WEBrick::HTTPServer#start: pid=2809 port=9876

http://localhost:9876/をブラウザで開く。

下記の様に表示されるはず。
スクリーンショット 2019-12-19 11.42.24.png

レスポンスヘッダもばっちり。
スクリーンショット 2019-12-19 11.42.31.png

簡単なRackアプリケーションはこんな感じ。シンプルである。

Rackを使ってみる2

Rackアプリケーションの起動方法として便利なコマンド、rackupがある。
rackupcongfig.ruというrack構成ファイルを元にRackアプリケーションを起動してくれる。

さっきのProcオブジェクトだとRailsをイメージしにくいので、簡単なクラスを定義してみる。
response_bodyとして、引数に渡されるWEBサーバーからのリクエストをjsonにして返そう。

my_app.rb
require 'json'

class MyApp

  def call(env)
    response_body = env.to_json

    response = [
      200,
      {"Content-Type" => "application/json; charset=utf-8"},
      [response_body]
    ]

    return response
  end

end

構成ファイルはこんな感じ。

config.ru
require './my_app'
run MyApp.new

後は下記コマンドで起動。

$ rackup config.ru

http://localhost:9292/を開けばjsonで下記が返ってくるはず。

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

Rackについてはこんな感じで説明は終わる。Railsに戻る。

RackによるRailsアプリケーションの起動

さて、RackがどうやってRackアプリケーションを起動するかわかったところでRailsの場合をみていく。
アプリを作成したディレクトリ直下に構成ファイルであるconfig.ruがある。先程の例同様、この構成ファイルを元にRackはRailsを起動するのである。

config.ru
# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application

先程例にあげた構成ファイルとほぼ同じ記述内容であることがわかるだろう。Rails.applicationはもちろんRails newして作ったアプリケーションのことだ。出力すればわかる。

config.ru
puts Rails.application.class
$ rails s
...
..
〜略〜
.
.

Adc2019::Application

さて、こうなるとRackアプリケーションとして必要なcallメソッドはどこに書いてあるの?という疑問がもちろんわく。
継承関係的に予想はつくけど、まあ念の為もう一度putsする。

config.ru
puts Rails.application.method(:call).source_location
/Users/かもひこ/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/railties-6.0.1/lib/rails/engine.rb

engine.rbで定義されているらしい。

engine.rb
def call(env)
  req = build_request env
  app.call req.env
end

こいつがRailsのRackアプリケーションとしてのcallメソッドとなる。以後、リクエストがくれば、このメソッドにそのリクエストが渡され実行される。

まとめ

流れがわかったところでまとめる。

処理の流れ
rails serverコマンドを叩く
server_command.rbのperformメソッドを実行
Rack::Server#startを呼び出す。
config.ruを元にRailsアプリケーションを起動。
以降、リクエストはengine.rbのcallメソッドで処理される。

長々と書いたが、やっていることは至ってシンプルだった。
RailsアプリケーションをRackアプリケーションとして起動しているだけ。これがrails serverの処理の流れかな。

最後に

長々とコードリーディングして得られたのは、RailsってよりRackについての知見な気がする。
まあ、ソースコードを読んだおかげでRailsへの「なんか動いている」感を少しでもなくせたのは良き収穫。
構成もなんとなく掴めたし、gemとか作ろうと思った時にこの経験は力になるだろう。

てわけで長々と読んでいただきありがとうございました。
間違い等あればコメント欄までお願いします。

ではおやすみ。

参考

railsコマンドを追う
Rack解説 - Rackの構造とRack DSL
Rackとは何か(1)Rackの生まれた背景
What’s Rack?

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

Rails, Dockerのマルチステージビルドに失敗した時の対応

問題

  • dockerのマルチステージビルドにてbundle installの内容をコピーしていた
    • COPY --from=builder /usr/local/bundle /usr/local/bundle
    • ここについての詳細は、Railsのdockerイメージを小さくする方法などで調べてください
  • Bundler 2.1.1, RubyGem 3.1.1がリリースされたことで、dockerビルド時のバージョンが勝手にアップデートされ、Railsが起動できなくなった

対応

  • /usr/local/lib もコピーする
COPY --from=builder /usr/local/lib /usr/local/lib
COPY --from=builder /usr/local/bundle /usr/local/bundle

詳細

エラーの詳細

  • nokogiriなどの with native extension なgemが全て読み込まれなくなっていた
    • 以下のようなエラーが発生
      • bundle実行時
        • Could not find nokogiri-1.10.7 in any of the sources
      • gem listなど実行時
        • Ignoring nokogiri-1.10.7 because its extensions are not built. Try: gem pristine nokogiri --version 1.10.7

対応について

  • /usr/local/lib をコピーしたものとしていないものを比較すると、/usr/local/lib/ruby/site_rubyの容量が変わっていたため、このあたりにnative extension周りのものが含まれると思われる(ここの詳細はわかっていません)
    • /usr/local/libではなく、/usr/local/lib/rubyのみのコピーでも動作可能
    • /usr/local/lib/ruby配下はsite_ruby以外に容量の変化はなかったため、/usr/local/lib/ruby/site_rubyまで指定する必要はないように思われる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あっさり読むrails①(image_tag)

はじめに

railsにおけるimage_tagの使い方を、極めてあっさり書いてみようと思います。
コードはHamlで書きます。

実行

image_tagは画像を表示するためのrailsヘルパーメソッドです。
例えば次の様に書きます。

= image_tag src = "画像のアドレス"

大きさや位置を指定したいなど、classをつける必要がある場合は次の様にします。

= image_tag src = "画像のアドレス", class: "クラス名"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者]DockerによるRails環境の構築(Windows)

初心者向けのDockerによるRails環境の構築です。
また、OSはWindowsです。

Windowsユーザーは、この環境構築の段階でとても苦しみますよね:sob:
私もとても時間がかかりました......

この記事がそういった方々の参考になれたら嬉しいです:v:

目次

Docker for Windowsのインストール

Dockerの公式サイトURL https://www.docker.com

以下の順に進める。

1.Product
2.GET DOCKER COMMUNITY EDITION
3.下にスクロール
4.DOCKER FOR WINDOWSのDownloud from Docker Store
5.Get Docker
6.実行>アイコン実行>
7.PowerShellで$ docker -vで確認
8.$ docker run hello-worldを実行
9.>Hello from Docker!と表示されればOK! :innocent:

Docker Toolboxのインストール

Docker ToolboxのダウンロードページURL
https://docs.docker.com/toolbox/toolbox_install_windows

1.Get Docker Toolbox for Windows > 実行 > VB||Gitはあるなら無視
2.\next > install > DQTを実行 > 待つ > $ docker -vで確認
3.$ docker run hello-worldを実行
4.>Hello from Docker!と表示されればOK! :innocent:

Rails開発環境構築用ファイル

docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - db-volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
volumes:
  db-volume:

Dockerfile
FROM ruby:2.4.5
RUN apt-get update -qq && apt-get install -y build-essential nodejs
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app

修正箇所)Dockerfile/ruby:2.4.5←任意のバージョンで

Docker Toolboxをお使いの場合の注意点

IPアドレスを確認するには、ターミナル(コマンドプロンプトなど)で次のコマンドを実行します。
$ docker-machine ip default
このコマンドの結果表示されたIPアドレスが、Docker Toolboxで動かしている仮想マシンのIPアドレスになります。
Docker Toolboxをお使いの場合は、このIPアドレスを使用して
http://<確認したIPアドレス>:3000
のようにアクセスしてください。

Railsの環境構築

docker-compose.ymlファイルの置いてあるディレクトリで実行。

1.Railsのコンテナを起動してRailsのプロジェクトを作成するコマンド
$ docker-compose run web rails new . --force --database=mysql

2.Railsイメージのビルド実行コマンド
$ docker-compose build

3.config/database.ymlの修正内容

default内の項目を修正
password: password
host: db

4.コンテナをデタッチドモード(バックグラウンド)で実行するコマンド
$ docker-compose up -d

5.RailsのコンテナでDB作成のタスクを実行するコマンド
$ docker-compose run web bundle exec rake db:create

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

ActiveModel::Attributesを使う

ここでは、Rails 5.2で導入されたActiveModel::Attributesについて紹介します。RubyのクラスにActiveRecordのカラムのような属性を加えられます。

サンプル:
https://github.com/kazubon/blog-rails6-vuejs/blob/master/app/forms/entries/form.rb

クラスにActiveModel::ModelとともにActiveModel::Attributesをインクルードします。

class Entries::Form
  include ActiveModel::Model
  include ActiveModel::Attributes

クラスメソッドattributeに属性名と型を渡すと、attr_accessorと同じように属性が使えるようになります。指定できる型の種類については、Railsのソースを直接参照してください。

https://github.com/rails/rails/blob/master/activemodel/lib/active_model/type.rb

  attribute :title, :string
  attribute :body, :string
  attribute :draft, :boolean, default: false
  attribute :published_at, :datetime
  attribute :tags
  attribute :number, :integer
  attr_accessor :user, :entry

attributeを使うと、「このクラスは何をパラメータとして受け取るのか」を明示できるというメリットもあります。なお、この例のtagsは配列で、指定できる型がないので型を指定していません。

属性を使うには、superでActiveModel::Attributesのinitializeメソッドを呼び出す必要があります。

  def initialize(user, entry, params = {})
    @user = user
    @entry = entry
    super(params)
  end

このsuper(params)は次のように置き換えても同じです。

    @attributes = self.class._default_attributes.deep_dup
    assign_attributes(params)

ActiveRecordのカラムと同じく、attributeで加えた属性に値を入れると、数値、時刻、ブーリアンのような型に合わせて値が変換されます。時刻(:datetime)では次のような感じです。

  form.published_at = "2019/12/19 12:30"
  form.published_at #=> 2019-12-19 12:30:00 +0900
  form.published_at = "xxxxx"
  form.published_at #=> nil

ブーリアン(:boolean)では、[false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF] が false、それ以外は true になります。空文字列はnilになります。

  form.draft = "1"
  form.draft #=> true
  form.draft = "0"
  form.draft #=> false

インスタンスメソッドattributesで「属性名 => 値」のハッシュが取り出せます。

form.attributes
=> {"title"=>"Foo", "body"=>"bar", "draft"=>false, "published_at"=>2019-12-15 15:40:00 +0900, "tags"=>["Ruby", "Rails"]}

インスタンスメソッドまたはクラスメソッドのattribute_namesで属性一覧が取り出せます。

form.attribute_names
=> ["title", "body", "draft", "published_at", "tags"]

以上です。

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

ActiveModel::Attributesを使う

ここでは、Rails 5.2で導入されたActiveModel::Attributesについて紹介します。RubyのクラスにActiveRecordのカラムのような属性を加えられます。

サンプル:
https://github.com/kazubon/blog-rails6-vuejs/blob/master/app/forms/entries/form.rb

クラスにActiveModel::ModelとともにActiveModel::Attributesをインクルードします。

class Entries::Form
  include ActiveModel::Model
  include ActiveModel::Attributes

クラスメソッドattributeに属性名と型を渡すと、attr_accessorと同じように属性が使えるようになります。指定できる型の種類については、Railsのソースを直接参照してください。

https://github.com/rails/rails/blob/master/activemodel/lib/active_model/type.rb

  attribute :title, :string
  attribute :body, :string
  attribute :draft, :boolean, default: false
  attribute :published_at, :datetime
  attribute :tags
  attribute :number, :integer
  attr_accessor :user, :entry

attributeを使うと、「このクラスは何をパラメータとして受け取るのか」を明示できるというメリットもあります。なお、この例のtagsは配列で、指定できる型がないので型を指定していません。

属性を使うには、superでActiveModel::Attributesのinitializeメソッドを呼び出す必要があります。

  def initialize(user, entry, params = {})
    @user = user
    @entry = entry
    super(params)
  end

このsuper(params)は次のように置き換えても同じです。

    @attributes = self.class._default_attributes.deep_dup
    assign_attributes(params)

ActiveRecordのカラムと同じく、attributeで加えた属性に値を入れると、数値、時刻、ブーリアンのような型に合わせて値が変換されます。時刻(:datetime)では次のような感じです。

  form.published_at = "2019/12/19 12:30"
  form.published_at #=> 2019-12-19 12:30:00 +0900
  form.published_at = "xxxxx"
  form.published_at #=> nil

ブーリアン(:boolean)では、[false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF] が false、それ以外は true になります。空文字列はnilになります。

  form.draft = "1"
  form.draft #=> true
  form.draft = "0"
  form.draft #=> false

インスタンスメソッドattributesで「属性名 => 値」のハッシュが取り出せます。

form.attributes
=> {"title"=>"Foo", "body"=>"bar", "draft"=>false, "published_at"=>2019-12-15 15:40:00 +0900, "tags"=>["Ruby", "Rails"]}

インスタンスメソッドまたはクラスメソッドのattribute_namesで属性一覧が取り出せます。

form.attribute_names
=> ["title", "body", "draft", "published_at", "tags"]

以上です。

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

rails データ保存あれこれ

基本

new → view → controller

  • 通常
def product_params
 params.require(:product).permit(:name, :desc)
end

fields_for


1対多で子要素もまとめて保存する

# prodcut.rb
 def product_params
   params.require(:product).permit(:name, :desc, production_conditions_attributes:[:name])
 end

class Product < ApplicationRecord
  has_many :production_conditions
  has_many :somethings, as: :somethingable
  accepts_nested_attributes_for :production_conditions

ポリモーフィック

  • commentableでnewする必要ありみたい

※参考記事
https://ruby-rails.hatenadiary.com/entry/20141207/1417926599

参考記事

https://qiita.com/Masato338/items/045788fdeadd776449d5

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

letを使った遅延処理、shared_examples

letを使った遅延処理

spec/system/tasks_spec.rb
require 'rails helper'

describe 'タスク管理機能', type: :system do
  describe '一覧表示機能' do
    let(:user_a) {FactoryBot.create(:user, name: 'ユーザーA', email: 'a@example.com')}
    let(:user_b) {FactoryBot.create(:user, name: 'ユーザーB', email: 'b@example.com')}

    before do
      FactoryBot.create(:task, name: '最初のタスク', user: user_a)
      visit login_path
      fill_in 'メールアドレス', with: login_user.email
      fill_in 'パスワード', with: login_user.password
      click_button 'ログインする'
    end
    context 'ユーザーAがログインしてる時' do
      let(:login_user) { user_a }

      it ~~~~ do
      end
    end
    context 'ユーザーBがログインしている時' do
      let(:login_user) { user_b }

      it ~~~~ do
      end
    end 
  end
end

このように先にログイン処理の大枠を書いておいて、contextごとにletで具体的なlogin_userの値を指定してる。

また、user_a,user_bの作成はbefore do end の外側で行ってる。

また、ログイン処理の大枠も、moduleに定義して他のファイルでも使い回せるようにした方がいいかも。

shared_examples

文字通り、exampleを定義して他でも簡単に使い回せるようにしよーってこと。
shared_contextのexample版

spec/system/tasks_spec.rb
require 'rails helper'

    before do
      FactoryBot.create(:task, name: '最初のタスク', user: user_a)
      visit login_path
      fill_in 'メールアドレス', with: login_user.email
      fill_in 'パスワード', with: login_user.password
      click_button 'ログインする'
    end

    shared_examples_for 'ユーザーAが作成したタスクが表示される' do
      it { expect(page).to have_content '最初のタスク' }
    end

    describe '一覧表示機能' do
      context 'ユーザーAがログインしてる時' do
       let(:login_userA) { user_a }
       it_behaves_like 'ユーザーAが作成したタスクが表示される'
      end
    end     

このようにdescribeよりも前にexampleを定義して使い回す。
これもmoduleのファイルを作ってそこに定義した方が良さそう。

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

特定のview(url)でグローバル変数を定義

こんにちは!

この前個人アプリを作成しているときにjs(jquery)とgem 'gon'について少しハマったのでご紹介しようと思います

なににハマったかと言うと、特定のビューの中で使用するjsの変数をグローバル変数として指定したく、jsのグローバル変数にgem'gon'で指定した値を代入したところ、アクションの外でエラーが出てしまい、他のjsファイルの実行の妨げになってしまったのです(ちょっと何言ってるかわかりづらくてすみません)

もう詳しい方ならこの一文を見ただけで察しはついているのではと思いますが、この時の状況と解決を簡単にご紹介します

'gon'とは

railsのコントローラで定義した変数をjs内で使用できるようにしてくれるgemです
https://qiita.com/s_nakamura/items/5d153f7d9db1b1190296

グローバル変数とは

〇〇.js
var test_global = hogehoge
$(function() {
   //処理
});

このようにjsの関数の外に変数を定義すると、どこでも使える変数を定義することができます(グローバル変数)
また、関数の中であっても'var'など付けず、変数名のみで変数を指定しても、グローバル変数として定義することができます

起こったこと

〇〇.js
var test_gon = gon.test_gon
$(function() {
   //この中で上記のグローバル変数を使いたい 
});

私はこんな感じでグローバル変数を定義しました
すると、このjsファイルが実行されるビューにいるとき(gonで変数を指定しているアクションを実行したとき)は問題ないのですが、それ以外のビューにいる場合エラーになってしまいます

よく考えればわかることですが、アクション外では当然gon.test_gonは定義されていないのでエラーとなるのです

えぇ・・といってもグローバル変数にしないとやりたいことが実装できない・・
と考え、ググっていたところいい条件分岐のやり方がありました

解決

〇〇.js
if(document.URL.match("/urlの文字列")) {
  //指定する文字列がURLに含まれる場合に実行する内容
  var test_gon = gon.test_gon;
}

こんな感じで指定してあげると、特定のビューで使用するグローバル変数を定義してあげることができます

ちなみに

〇〇.js
if(document.URL.match("/urlの文字列|/urlの文字列")) {
  //指定する文字列がURLに含まれる場合に実行する内容
  var test_gon = gon.test_gon;
}

こうやって書くと、例えば/文字列/:test_id/文字列/:gon_id/のようなurlを指定することもできます
https://haayaaa.hatenablog.com/entry/2019/02/17/144329

本当jsてなんでもできちゃうのね

ともあれ指定できてよかったです

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

(rails)特定のview(url)でグローバル変数を定義

こんにちは!

この前個人アプリを作成しているときにjs(jquery)とgem 'gon'について少しハマったのでご紹介しようと思います

なににハマったかと言うと、特定のビューの中で使用するjsの変数をグローバル変数として指定したく、jsのグローバル変数にgem'gon'で指定した値を代入したところ、アクションの外でエラーが出てしまい、他のjsファイルの実行の妨げになってしまったのです(ちょっと何言ってるかわかりづらくてすみません)

もう詳しい方ならこの一文を見ただけで察しはついているのではと思いますが、この時の状況と解決を簡単にご紹介します

'gon'とは

railsのコントローラで定義した変数をjs内で使用できるようにしてくれるgemです
https://qiita.com/s_nakamura/items/5d153f7d9db1b1190296

グローバル変数とは

〇〇.js
var test_global = hogehoge
$(function() {
   //処理
});

このようにjsの関数の外に変数を定義すると、どこでも使える変数を定義することができます(グローバル変数)
また、関数の中であっても'var'など付けず、変数名のみで変数を指定しても、グローバル変数として定義することができます

起こったこと

〇〇.js
var test_gon = gon.test_gon
$(function() {
   //この中で上記のグローバル変数を使いたい 
});

私はこんな感じでグローバル変数を定義しました
すると、このjsファイルが実行されるビューにいるとき(gonで変数を指定しているアクションを実行したとき)は問題ないのですが、それ以外のビューにいる場合エラーになってしまいます

よく考えればわかることですが、アクション外では当然gon.test_gonは定義されていないのでエラーとなるのです

えぇ・・といってもグローバル変数にしないとやりたいことが実装できない・・
と考え、ググっていたところいい条件分岐のやり方がありました

解決

〇〇.js
if(document.URL.match("/urlの文字列")) {
  //指定する文字列がURLに含まれる場合に実行する内容
  var test_gon = gon.test_gon;
}

こんな感じで指定してあげると、特定のビューで使用するグローバル変数を定義してあげることができます

ちなみに

〇〇.js
if(document.URL.match("/urlの文字列|/urlの文字列")) {
  //指定する文字列がURLに含まれる場合に実行する内容
  var test_gon = gon.test_gon;
}

こうやって書くと、例えば/文字列/:test_id/文字列/:gon_id/のようなurlを指定することもできます
https://haayaaa.hatenablog.com/entry/2019/02/17/144329

本当jsてなんでもできちゃうのね

ともあれ指定できてよかったです

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