- 投稿日:2019-12-19T23:56:25+09:00
[WIP]ActiveRecordを使い、複数テーブルへBULK INSERTする方法
目的
- Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
- 今後、railsを学ぶ方に向けての参考に役立てる。
前提条件
実行環境
- Ruby 2.5.1
- Rails 5.2.3
- MySQL 5.7
gem
想定
- 大量のデータ(CSVファイル)をseedで一括で取り込みたい
手順
- CSVファイルを設置する
- seeds.rbにCSVをBULK INSERTするコードを記述
- 各modelにオプションを設定
rails db:seed(完)1. CSVファイルを設置する
db ├── data │ ├── hoge.csv ← 初期データとして取込みたいCSVファイルを設置 │ └── fuga.cs ← 初期データとして取込みたいCSVファイルを設置 ├── migrate │ ├── 20191118191522_create_huges.rb │ └── 20191119060537_create_fugas.rb ├── schema.rb └── seeds.rb2. seeds.rbにCSVをBULK INSERTするコードを記述
db/data/seeds.rb# csvライブラリを読み込みます。 require 'csv' # 冪等性を担保するためトランザクション処理する # 冪等性とは「同じ操作を何度繰り返しても、同じ結果が得られる性質」のこと ActiveRecord::Base.transaction do import = ->(klass, file_path) do options = { encoding: "UTF-8", headers: true, header_converters: klass.csv_header_converters, converters: klass.csv_converters } # 登録件数が多いことを想定して念のため1000件ずつ処理を行うように設定 CSV.read(file_path, options).each_slice(1000) do |rows| values = rows.map(&:to_h).map { |row| klass.new(row) } klass.import! values, validate: true end end import.(Hoge, Rails.root.join('db', 'data', 'dataset_50.csv')) import.(Fuga, Rails.root.join('db', 'data', 'house_data.csv')) end3. 各modelにオプションを設定
app/models/hoge.rbclass Hoge < ApplicationRecord belongs_to :fuga validates :label, :fuga_id, :year, :month, presence: true class << self def csv_header_converters -> (name) { { ID: :id, Label: :label, Fuga: :fuga_id, Year: :year, Month: :month, }[name.to_sym] || raise } end def csv_converters nil end end endapp/models/fuga.rbclass Fuga < ApplicationRecord has_many :hoge validates :firstname, :lastname, :city, presence: true validates :has_child, inclusion: { in: [true, false] } class << self def csv_header_converters -> (name) { { ID: :id, Firstname: :firstname, Lastname: :lastname, City: :city, Num_Of_People: :num_of_people, Has_Child: :has_child }[name.to_sym] || raise } end def csv_converters -> (field, field_info) { if field_info.header == :has_child field == 'Yes' else field end } end end end4.
rails db:seed(完)bashrails db:seed関連URL
所感
久しぶりに書いた。業務でCSVインポートの処理を実装するの何気に多い。
後で再インプットも兼ねて解説コメント丁寧に書く。
気力があれば過去記事のメンテナンスしていく
- 投稿日:2019-12-19T23:56:25+09:00
[WIP]ActiveRecordを使って複数テーブルへBULK INSERTする方法
目的
- Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
- 今後、railsを学ぶ方に向けての参考に役立てる。
前提条件
実行環境
- Ruby 2.5.1
- Rails 5.2.3
- MySQL 5.7
gem
想定
- 大量のデータ(CSVファイル)をseedで一括に取り込みたい
手順
- CSVファイルを設置する
- seeds.rbにCSVをBULK INSERTするコードを記述
- 各modelにオプションを設定
rails db:seed(完)1. CSVファイルを設置する
db ├── data │ ├── hoge.csv ← 初期データとして取込みたいCSVファイルを設置 │ └── fuga.csv ← 初期データとして取込みたいCSVファイルを設置 ├── migrate │ ├── 20191118191522_create_huges.rb │ └── 20191119060537_create_fugas.rb ├── schema.rb └── seeds.rb2. seeds.rbにCSVをBULK INSERTするコードを記述
db/data/seeds.rb# csvライブラリを読み込みます。 require 'csv' # 冪等性を担保するためトランザクション処理する # 冪等性とは「同じ操作を何度繰り返しても、同じ結果が得られる性質」のこと ActiveRecord::Base.transaction do import = ->(klass, file_path) do options = { encoding: "UTF-8", headers: true, header_converters: klass.csv_header_converters, converters: klass.csv_converters } # 登録件数が多いことを想定して念のため1000件ずつ処理を行うように設定 CSV.read(file_path, options).each_slice(1000) do |rows| values = rows.map(&:to_h).map { |row| klass.new(row) } klass.import! values, validate: true end end import.(Hoge, Rails.root.join('db', 'data', 'hoge.csv')) import.(Fuga, Rails.root.join('db', 'data', 'fuga.csv')) end3. 各modelにオプションを設定
app/models/hoge.rbclass Hoge < ApplicationRecord belongs_to :fuga validates :label, :fuga_id, :year, :month, presence: true class << self def csv_header_converters -> (name) { { ID: :id, Label: :label, Fuga: :fuga_id, Year: :year, Month: :month, }[name.to_sym] || raise } end def csv_converters nil end end endapp/models/fuga.rbclass Fuga < ApplicationRecord has_many :hoge validates :firstname, :lastname, :city, presence: true validates :has_child, inclusion: { in: [true, false] } class << self def csv_header_converters -> (name) { { ID: :id, Firstname: :firstname, Lastname: :lastname, City: :city, Num_Of_People: :num_of_people, Has_Child: :has_child }[name.to_sym] || raise } end def csv_converters -> (field, field_info) { if field_info.header == :has_child field == 'Yes' else field end } end end end4.
rails db:seed(完) ?bashrails db:seed関連URL
所感
久しぶりに書いた。業務でCSVインポートの処理を実装するの何気に多い。
後で再インプットも兼ねて解説コメント丁寧に書く。
気力があれば過去記事のメンテナンスしていく
- 投稿日:2019-12-19T23:54:22+09:00
RailsでJSON REST APIを実装する(GET編)
背景
現在、RailsのapiモードでJSON APIサーバを開発している。
RESTful APIの原則に従い、各種リソースに対するGET、PUT、POST、DELETEを実装している。今回はGETの実装について書きます。
やりたいこと
クエリパラメータでリソースの属性を指定し、条件に合うリソースを全てGETするJSON APIを実装したい。
GET /staffs?store_id=12345678&gender=male&grade=professionalクエリパラメータには
store_id、gender、gradeを設定できる。全てのパラメータを記述しなくてもよく、任意のパラメータだけでリソースを絞ることを可能にしたい。
少し悩んだ点
例えば、
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終わり。
- 投稿日:2019-12-19T23:48:57+09:00
【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)に対応してるからうまくいかないよ!という内容。解決法
Gemfilegem 'sass-rails', '~> 5.0'↓
Gemfilegem 'sass-rails'↓
$ bundle installバージョン指定を外してあげれば無事通るようになりました。
おわりに
最後まで読んで頂きありがとうございました
どなたかのエラー解決時間の短縮につながればと思います。
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-12-19T23:43:59+09:00
Rspecつまづきポイント!
この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc. Advent Calendar 2019 20日目の記事です。
はじめに
ここ一年ほど、社内で自動テスト推進係をしております@ysysysysがお送りいたします。
社内の各プロジェクト、特にRailsプロジェクトでRspecを書きましょうと皆さんとともに日々頑張っております。
今回はRspecを書いていく中で個人的につまづきポイントだったなと思うところをいくつかピックアップしてその対応をご紹介しようかと思います。その1 let と let!
Rspecではletという機能があります。Spec内ではインスタンス変数で定義するよりもlet(:hoge)と定義するのが一般的なようです。
letをつかえば、その内容はメモリーされ、exampleのたびに再生成するコストが減るとのこと。
ここでつまづいたのが、このletが遅延評価するという特性による挙動です。user_factory.rbFactoryBot.define do factory :user do first_name { '太郎' } last_name { '山田' } email {'testuser@example.com'} end endこのようにFactoryBotでUserを作り、そのModelのテストを行うとします。
user_spec.rbdescribe '#full_name' do let(:user) { create(:user) } it '山田太郎を返す' do expect(user.full_name).to eq '山田太郎' end endところがこれでは正常に動作しないです。
letが評価されるタイミングはuserを呼び出した時です。なのでfull_name実行時にはuserは作られておらずうまくいかないのです。user_spec.rbdescribe '#full_name' do let!(:user) { create(:user) } it '山田太郎を返す' do expect(user.full_name).to eq '山田太郎' end endletの後ろに!をつけることで、itの中身が実行される前に中身が作られるようにするとうまくいきます。
その2 サブドメインのテスト
こんな感じのルートがあった時に、サブドメイン配下のRequest specを書きたい場合はどうすればいいかハマりました。
routes.rbconstraints subdomain: 'hoge' do get '/' => subdomain#index end正解はこちら⬇︎
subdomain_request_spec.rbdescribe 'GET index' do it "200を返すこと。" do headers = { "Host" => "hoge.example.com" } get '/', headers: headers expect(response).to have_http_status(:ok) end endこのようにgetする際のheaderにHostを引っ付けてあげればいけるんですね〜
確認するとちゃんとサブドメインが取れています。pry(#<RSpec::ExampleGroups::SubdomainController>) request.subdomain => "hoge"その3 モバイルのテストをしたい!
テンプレートをPCかスマホで出し分けている時、スマホでもテストをしたい場合はどうすればいいかです。
結論から言うとこれもheadersをいじります。request_spec.rbdescribe 'GET index' do it "スマホで200を返すこと" do headers = { "Host" => "HTTP_USER_AGENT" => "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X)" } get '/', headers: headers expect(response).to have_http_status(:ok) end endこれでモバイルだけおかしい!みたいなこともなく安心。
その4 セッションを持たせておきたい
ログイン先のページをテストする際などセッションがあることを前提とした部分をてすとしたいときです。
こうしましょうrequest_spec.rbdescribe 'GET mypage' do setup do post "/login", params: {email: 'test@example.com', pass: 'testtest' } end it "200を返すこと" do get '/mypage' expect(response).to have_http_status(:ok) end endsetupブロックで事前に/loginにpostしておくことでセッションを生成しておくことができます。
そのあとにexampleを書いていけばOK!その5 CarrierWaveを使っているところのテスト
Carrierwaveを使っていて画像をアップロードする部分のテストはどうすればいいの!
upload_request_spec.rbbefore(:each) do allow_any_instance_of(ImageUploader).to receive(:store!).and_return(true) end let(:params) do { image: { file: Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/images/test.jpg')), } } end it 'Imageがアップロードされている。' do post '/upload', params: params expect(Image.count).to eq 1 endテスト用の画像ファイルをプロジェクト配下のどこかに置いておいて、読み込んであげればOKです。
私の場合は画像をS3にアップしていたのでその処理をスキップするためにUploaderのstore!メソッドをスタブしました。
本当は環境ごとに適切に分けておいた方がいいんでしょうが、、まとめ
Rspecを書いていく中で、個人的に印象に残っているつまづきポイントをざっと紹介しました。
こんな感じでつまづくところはつまづきつつ、日々テストコード書いてプロダクトの品質向上に努めております。お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はエイチームグループ採用ページよりご応募くださいませ。Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム、社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能でございます。
よろしくお願いいたします!
明日は
明日は弊社技術開発部の貴公子@nao_70さんの登場です。お楽しみに。
- 投稿日:2019-12-19T22:46:53+09:00
「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 homeroutes.rbRails.application.routes.draw do get '/', to: 'home#homw' endviewsは適当で
home.html.erb<h1>HOME</h1>んでrails s
$ rails slocalhost:3000にアクセス
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.rbrequire "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メソッドの記述部分。コマンドラインで叩いたserverをinvokeに渡している。直略で呼び出すの意味を持つこのメソッド。
次に見るんはここやな。てわけで
invokeメソッドを見てみる。command.rb
rails/railties/lib/rails/command.rbdef 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.rbdef 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.rbdef 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している。そろそろメソッド一つ一つ追っていくのがだるくなったのでpathをputsして確認する。
(できればソースを直にいじるべきではないが、パッチをあてるのもあれだったので。)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.rbdef 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).
つまり、
- callメソッドを持つこと。
- 引数としてWEBサーバーからのHTTPリクエストを受けること
- 返り値は、ステータスコード(100以上)、レスポンスヘッダ(ハッシュ)、レスポンスボディであること。
の3つがRackアプリケーションとしての最低限の条件である。
それで
irbで簡単に作ってみる。(ここは参考元のほぼ丸パクリ。)$ irbまず、
rackを読み込む。irb(main):001:0> require 'rack' => truecallメソッドを持っていればよいので、
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=9876http://localhost:9876/をブラウザで開く。
簡単なRackアプリケーションはこんな感じ。シンプルである。
Rackを使ってみる2
Rackアプリケーションの起動方法として便利なコマンド、
rackupがある。
rackupはcongfig.ruというrack構成ファイルを元にRackアプリケーションを起動してくれる。さっきのProcオブジェクトだとRailsをイメージしにくいので、簡単なクラスを定義してみる。
response_bodyとして、引数に渡されるWEBサーバーからのリクエストをjsonにして返そう。my_app.rbrequire '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.rurequire './my_app' run MyApp.new後は下記コマンドで起動。
$ rackup config.ruhttp://localhost:9292/を開けばjsonで下記が返ってくるはず。
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.ruputs Rails.application.class$ rails s ... .. 〜略〜 . . Adc2019::Applicationさて、こうなるとRackアプリケーションとして必要な
callメソッドはどこに書いてあるの?という疑問がもちろんわく。
継承関係的に予想はつくけど、まあ念の為もう一度putsする。config.ruputs 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.rbdef 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?
- 投稿日:2019-12-19T20:05:59+09:00
RailsロガーからStackdriverに切り替える
はじめに
- 当該記事はたたき台のため、追記予定
システム情報
- Ruby 2.6.5
- Rails 5.2.3
- bundler 1.17.2
- puma
- rbenv local環境下
Terminal$ rbenv versions system 2.4.1 * 2.6.5 (set by /Users/%user%/%repository%/.ruby-version) $ bundle exec rails -v Rails 5.2.3Stackdriverに切り替える
これまでの構成
- Rails.loggerのformatterを
config gemで環境毎に設定(或いはsettings.ymlでも良い)config/settings/development.yml…途中省略… # logging setting # fatal システムクラッシュ(システムエラー) # error アプリケーションエラー # warn 警告 # info プログラム通知 # debug 開発者向けの情報 logger: formatter: 'proc {|severity, datetime, progname, msg| "severity:[#{severity.rjust(5, " ")}]\tdatetime:#{datetime.strftime("%Y/%m/%d %H:%m:%S")} pid:#{$$}\tmessage:#{msg}\n"}'production.rb|development.rbconfig.log_formatter = eval Settings.logger.formatterLogseverity:[ INFO] datetime:2019/12/19 00:00:00 pid:20026 message:Started GET "/api_v2/users?first_name=ほげお&gender=male&zip_code=100-0001" for ::1 at 2019-12-19 00:00:00 +0900 severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: (8.5ms) SET NAMES utf8, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483 severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: ↳ vendor/bundle/ruby/2.6.0/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98 severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: (8.2ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC severity:[DEBUG] datetime:2019/12/19 00:00:00 pid:20026 message: ↳ vendor/bundle/ruby/2.6.0/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98 severity:[ INFO] datetime:2019/12/19 00:00:01 pid:20026 message:Processing by ApiV2::UsersController#index as */*これに
logrotateの組み合わせでも全く構わないとは思うのですが、今回はこれをStackdriverに切り替えたいと思います。Stackdriverとは
- モニタリング、ロギング、および診断機能を提供する管理ツールスイート
- 詳しくはGitHub のドキュメント
- 用途を大きく分類すると、Stackdriver Logging AgentとLogging ライブラリとなる
- Stackdriver Logging AgentとLogging ライブラリの導入どちらかによってStackdriverの運用方法が大きく異なる
スイートに含まれるパッケージ・ライブラリ
Stackdriver Debugger
- コードの動作を調査し、デバッグを行える
- gem を組み込んだら、Cloud Console に移動し、コードをアップロードします(または、Stackdriver Debugger でリポジトリ内のコードを指定します)。すると、実行中のアプリケーションの変数値とスタックトレースを含むスナップショットを取得
- 実行中のアプリケーションに任意のログ文を追加することも可能
Stackdriver Error Reporting
- 例外を検出して報告する Google Cloud ツール
- アプリケーションのクラッシュを捉え、それを論理的にグループ化して通知してくれる
- UI(エラー管理インターフェース)に整理してわかりやすく表示
- UI には、エラーのスタックトレースや、各クラッシュのログおよび分散トレースへのリンクが表示される
- グループ化されたバグ データベースから発生したバグを関連づけることができる
Stackdriver Trace
- アプリケーションのパフォーマンス モニタリングと分散トレーシングを行う
- リクエストがデータベースに到達し、ビューがレンダリングされ、アプリケーション ロジックの実行に要した時間が自動的に表示される
- リクエストがマイクロサービス アーキテクチャでどのように処理されるかを表示
- レイテンシの時系列の傾向に関する詳細レポートを提供
- 最新リリースへの更新後、アプリケーションに遅延はないかチェックできる
Stackdriver Logging
- GA
- ログデータのフルマネージドサービス
- ログデータのリアルタイム分析に対応
- fluentd ロギング エージェントとともに利用される
- Container Engine を使っていなくても利用できる
- Ruby Logger や Rails Logger の代わりに簡単に導入できる
- スケーラブル設計(1秒あたりの処理可能なデータサイズはテラバイト)
- スタートガイド
- API リファレンス
- 紹介記事
導入手順
2. GCPプロジェクト上にStackdriver Logging APIをインストールする
3. Gemを追加する
Terminalvi GemfileGemfile# logging gem 'stackdriver-ruby', require: 'stackdriver' gem 'stackdriver'4. Bundle install (gem installでも可)
Terminal$ bundle _1.17.2_ install --path vendor/bundle Fetching gem metadata from https://rubygems.org/......... Fetching gem metadata from https://rubygems.org/. Resolving dependencies... Using rake 13.0.0 …途中省略… Fetching debug_inspector 0.0.3 Installing debug_inspector 0.0.3 with native extensions Fetching binding_of_caller 0.8.0 Installing binding_of_caller 0.8.0 with native extensions …途中省略… Using listen 3.1.5 Fetching multi_json 1.14.1 Installing multi_json 1.14.1 Using mysql2 0.5.2 …途中省略… Fetching google-cloud-env 1.3.0 Installing google-cloud-env 1.3.0 Fetching google-cloud-core 1.4.1 Installing google-cloud-core 1.4.1 Fetching google-protobuf 3.11.2 (universal-darwin) Installing google-protobuf 3.11.2 (universal-darwin) Fetching googleapis-common-protos-types 1.0.4 Installing googleapis-common-protos-types 1.0.4 Fetching grpc 1.25.0 (universal-darwin) Installing grpc 1.25.0 (universal-darwin) Fetching googleapis-common-protos 1.3.9 Installing googleapis-common-protos 1.3.9 Fetching jwt 2.2.1 Installing jwt 2.2.1 Fetching memoist 0.16.2 Installing memoist 0.16.2 …途中省略… Using spring-watcher-listen 2.0.1 Fetching stackdriver-ruby 0.0.1 Installing stackdriver-ruby 0.0.1 …途中省略… Fetching os 1.0.1 Installing os 1.0.1 Fetching signet 0.12.0 Installing signet 0.12.0 Fetching googleauth 0.10.0 Installing googleauth 0.10.0 Fetching rly 0.2.3 Installing rly 0.2.3 Fetching google-gax 1.8.1 Installing google-gax 1.8.1 Fetching stackdriver-core 1.4.0 Installing stackdriver-core 1.4.0 Fetching google-cloud-logging 1.9.1 Installing google-cloud-logging 1.9.1 Fetching google-cloud-debugger 0.35.1 Installing google-cloud-debugger 0.35.1 with native extensions Fetching google-cloud-error_reporting 0.34.0 Installing google-cloud-error_reporting 0.34.0 Fetching google-cloud-trace 0.36.1 Installing google-cloud-trace 0.36.1 …途中省略… Using uglifier 4.2.0 Using web-console 3.7.0 Bundle complete! 31 Gemfile dependencies, 135 gems now installed. Bundled gems are installed into `./vendor/bundle`5. Railsコンフィグに追加する
application.rb# Stackdriver logging settings require "google/cloud/logging" Google::Cloud.configure do |config| # Stackdriver Logging specific parameters config.logging.project_id = "YOUR-PROJECT-ID" config.logging.keyfile = "/path/to/service-account.json" end6. 起動確認
Terminal$ bundle exec rails s -p 8080 Puma starting in single mode... * Version 3.12.1 (ruby 2.6.5-p114), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://localhost:8080 Use Ctrl-C to stop使い方は追って記載します
- 投稿日:2019-12-19T19:54:35+09:00
俺たちは雰囲気でAdminLTEを使っている
はじめに
※この記事は、「RUNTEQ Advent Calendar 2019」、19日目の記事です。
普段、Webサービス・アプリのUIデザインを仕事としてやっているのですが、管理画面の制作は、エンジニアの誰かが良しなに対応してくれているだけという状況がかなり多いです。
社内のRailsエンジニアと話していたときに、「AdminLTEなんかもっといい感じに使えたいんだよね」というお悩み相談を受けたことがあります。
今回は、AdminLTEを使用する際に持っていると助かりそうな知識と、ちょっとしたテクニックをまとめてみました。デザイナーがいないプロジェクトでも、CSSフレームワーク等を用いてせっせと管理画面をつくってくれている、最高のエンジニアの方々のお役に立てたら最高です。
AdminLTEとはなにか
AdminLTEは、Bootstrapベースでつくられた、管理画面作成などに特化したCSSフレームワークです。
これから使うならバージョン3以降がおすすめです。骨組みはBootstrapで、それにスタイリングを加えて、サンプルも提供してくれているものがAdminLTEと考えてもらえると良いかなと思います。
AdminLTE 3の画面サンプルはこちら。
https://adminlte.io/themes/dev/AdminLTE/index.htmlこれはv3.0.0の状態でつくられていて、Bootstrapはv4.3.1、FontAwesomeはv5.11.2が内包されています。
コード一式を手元にダウンロードしたい場合は、GitHubリポジトリからも可能です。
https://github.com/ColorlibHQ/AdminLTE/releases/tag/v3.0.0こんなお悩みありませんか?
いい感じといわれても、ズバッと一言でお答えするのが難しかったので、悩みを集めるところからはじめました。
飲み会や休憩中を狙ってヒアリングした中で出てきたものや、コードをのぞいてて困ってそうだなあと思った事柄はこんな感じでした。お悩み
- いっぱいサンプルがあるけど、どれを使えばいいかわからない
- とりあえずスマホの表示が崩れない状態になりたい
- フッターが欲しいとか途中で言われてかなしい
- ページのタイトルをどうつければいいかわからない
- サイドメニューがなんかいい感じにならない
- テーブルにめっちゃボタンが並んでてやばい
- 入力項目が多すぎてめっちゃ大変に見える
- たまに要素間の余白がなくなったりする
- アラートどこにどんな感じで出したら最高なのかよくわかんない
- 数字がみづらい
- ちょっとだけカスタマイズしたいんだけどどうしよう
個別に答えていくのもできるかもしれませんが、方針やテクニックを知っていれば、解決できそうな予感がします。
あわよくば応用もできるのではと思い、いったんまとめ直してみました。
お答え
- 基本構造を知る
- 便利なclassを使いこなす
- 導線のつくりかたを統一する
- そのページでやれることを絞る
- 項目のグループをつくる
- 上書き用のcssファイルをつくる
解説
順番にみていきます。
基本構造を知る
AdminLTEの構造
AdminLTEのデフォルトサンプルは、ヘッダー、サイドバー、コンテンツ、フッターで構成されています。
body .wrapper nav.main-header aside.main-sidebar .content-wrapper section.content-header .container-fluid section.content .container-fluid # ここらへんにグリッドレイアウトの骨組みが入っていく aside.control-sidebar # サンプルではカスタマイズ用のサイドバー footer.main-footer #sidebar-overlay # スマホサイズでサイドメニュー閉じる用のオーバーレイこの形を崩さなければ、基本構成はほぼ完成です。
Bootstrapのグリッドレイアウト
コンテンツ部分はBootstrapのグリッドレイアウトで組みます。
.rowは入れ子にすることもできます。.container-fluid .row .col-md-6 # 全体の50% .row .col-md-6 # 全体の50%の50%=25% .row .col-md-6 .col-md-6いったん
.col-md-xxだけ使えていれば大丈夫なのですが、横幅によって組み方を変えたい場合はオプションがあったりもします。
- Extra small Small Medium Large Extra large Breakpoint <576px ≥576px ≥768px ≥992px ≥1200px Class .col-xs-xx .col-sm-xx .col-md-xx .col-lg-xx .col-xl-xx 便利なclassを使いこなす
AdminLTE自体にも様々なコンポーネントが用意されています。
ただ、骨組みであるBootstrapには、borderをつけたり余白をつけたりするための便利classが存在して、細かい調整が可能です。例えば、borderだったら
.border-xx。余白だったら.mt-xxや.pr-sm-xxのようなものです。
他には、.alertや.text-rightなんかもよく使います。このあたりを組み合わせると、めっちゃよく使うテーブルのレイアウトを、サンプルよりもうちょっと良い感じにできます。
テーブルレイアウト
Bordered Table
プロジェクトでよくみかけるのは、ボーダーつきのテーブルです。
.card .card-header .card-title .card-tools .card-body table.table.table-bordered thead tbody .card-footerただ、このままだとレスポンシブ対応してません。
そんなときは.card-bodyに.table-responsiveをつけるといい感じになるかと思いきや、上部の線がいなくなってしまいます。.card .card-header .card-title .card-tools .card-body.table-responsive table.table.table-bordered thead tbody .card-footerそんなときは、こうするといい感じです。
.card .card-header .card-title .card-tools .card-body.table-responsive table.table.table-bordered.border-top thead tbody .card-footer良い感じのテーブルができました。
これを鬼のようにコピペすると、管理画面はほぼ完成です。
あとはちょっとしたテクニックですが、数値を表示するときは、小数点以下の桁数を揃えて
.text-rightをつけると見やすくなります。table.table.table-bordered.border-top thead tr th.text-right tbody tr td.text-right導線のつくりかたを統一する
いったんCRUDどおりにやって悩まないのが良いと思います。
サイドメニューに、ユーザー管理という区分を設けて、一覧・作成を配置します。
コンテンツ部分はサンプル通りに、タイトル、パンくず、コンテンツを並べて、このレイアウトを使い回すつもりでやりましょう。
基本の骨組みを省こうとすると、別の調整が必要になるので、いったんは教科書どおりに使うべきです。
そのページでやれることを絞る
一覧ページじゃなくて、詳細ページにだけあればいいものをトルという方針でやるといいです。
一覧ページでは詳細・編集ボタン、詳細ページでは編集・複製・削除ボタンのように区別をするといい感じです。さらに、IDやタイトル名にリンクを貼ると、詳細ボタンを省略できて、アクション部分がスッキリします。
※あくまでも指針なので、複製・削除をすることがものすごく多いのであれば、一覧ページにも置くべきではあります
項目のグループをつくる
例えば、ユーザー新規登録ページがあったとします。単純に項目を並べると、こんな感じになります。
入力項目などが多数ある場合は、似たものを近づけてグループ化してあげると、心理的な負担が和らぎます。
そのグループごとに、見出しをつけるといい感じです。ページ自体は長くなってしまうのですが、小さいゴールがあると、ツラさが少し和らぎます。
ページが長くなるのがどうしてもツラい場合は、レイアウトを変えると、多少短くはできます。
もっと使いやすくするなら、入力エリアは入力量に近い見た目にするといいです。ゴールや負荷が見えたほうが、タスクには取り組みやすくなります。
上書き用のcssファイルをつくる
フォントの指定を変えたり、ちょっとした色味の変更、新しいパーツの追加がしたくなるときがあったりします。
そんなときは、大元のcssをいじるのではなく、新規で上書き用のファイルをつくるといいです。assets内に
_override.scssのようなファイルをつくるのがおすすめです。bodyに
.overrideというclassをつけておくと、セレクタの詳細度があがるのて、上書きがしやすくなる利点もあります。※
.overrideじゃなくて、.customとか.patchとかでもいいかも。body.override ... .container.override { .container { ... } }AdminLTEやBootstrapのドキュメントをみて、使いたいコンポーネントが見つからなかった場合は、最悪自分で書いちゃっていいかなとも思います。HTML&CSSが書ける場合ですが。
その場合、プロジェクトの頭文字などを接頭辞としてつけておくと、class名かぶりを避けやすくなります。
.rt-container { ... }まとめ
というわけで、AdminLTEを使うときに知っていると良さそうな事柄をまとめてみました。
- 基本構造を知る
- 便利なclassを使いこなす
- 導線のつくりかたを統一する
- そのページでやれることを絞る
- 項目のグループをつくる
- 上書き用のcssファイルをつくる
「俺たちは雰囲気でAdminLTEを使っている」とは思いますが、ちょっとした知識を武器にして、もう少しだけでも良い雰囲気で使えるようになってもらえたら最高です。
その他
他にも何か質問がきたり、思いついたりしたら、ここに簡易的なお答えを追記していきます。
- ダッシュボードにはとりあえずサイトマップ的なものを置いておくと便利かも
- layoutファイルにはどこまで記述するか
.content-wrapperまでで、その先は個別のviewファイルに分けるのがいいかなあと思いますおまけ
なぜAdminLTEを使うかの補足
いろいろ理由は並べられるけど、とにかく秒で作り切ることが大事です。
サービスやプロダクトは、管理画面自体をつくりこんでも、ユーザーへの提供価値に直接つなげづらいことが多いです。単純に、エンジニアやデザイナーがじっくりと時間をかけてつくる余裕がなかったりもします。
少ない時間でなんとか最低限の使い勝手も担保したいけれど、バックエンド・サーバーサイドのエンジニアは他にもやることがたくさんあって、CSS、JavaScriptの習得まで手が回りづらいという状況もある気がします。
そんなときにBootstrapやAdminLTEのように、パーツの組み合わせだけでつくりきれるフレームワークが重宝されるのだと思います。
ただ、管理担当者が、素早いオペレーションをできることには十分に価値があるので、いい感じに手軽に管理画面をつくりたい俺たちにとって、AdminLTEのようなものは最高のCSSフレームワークです。
レイアウトの指針
レイアウトの4つの基本原則だけでも知っていると、さらに応用がききそうなので、少しだけふれておきます。
還俗は、近接、整列、反復、対比の4つです。
①近接→関連があるものは近づける、関連がないものは余白をつける
②整列→端をそろえる
③反復→繰り返す
④対比(コントラスト)→違うものには差をつけるきちんと知りたい方は、ノンデザイナーズデザインブックという書籍を読むと最高です。
- 投稿日:2019-12-19T17:44:35+09:00
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 sourcesgem 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まで指定する必要はないように思われる
- 投稿日:2019-12-19T16:56:26+09:00
あっさり読むrails①(image_tag)
- 投稿日:2019-12-19T15:53:43+09:00
[初心者]DockerによるRails環境の構築(Windows)
初心者向けのDockerによるRails環境の構築です。
また、OSはWindowsです。Windowsユーザーは、この環境構築の段階でとても苦しみますよね
私もとても時間がかかりました......この記事がそういった方々の参考になれたら嬉しいです
目次
- Docker for Windowsのインストール
- Docker Toolboxのインストール
- Rails開発環境構築用ファイル
- Docker Toolboxをお使いの場合の注意点
- Railsの環境構築
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!Docker Toolboxのインストール
Docker ToolboxのダウンロードページURL
https://docs.docker.com/toolbox/toolbox_install_windows1.Get Docker Toolbox for Windows > 実行 > VB||Gitはあるなら無視
2.\next > install > DQTを実行 > 待つ >$ docker -vで確認
3.$ docker run hello-worldを実行
4.>Hello from Docker!と表示されればOK!Rails開発環境構築用ファイル
docker-compose.ymlversion: '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:DockerfileFROM 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=mysql2.Railsイメージのビルド実行コマンド
$ docker-compose build3.config/database.ymlの修正内容
default内の項目を修正
password: password
host: db4.コンテナをデタッチドモード(バックグラウンド)で実行するコマンド
$ docker-compose up -d5.RailsのコンテナでDB作成のタスクを実行するコマンド
$ docker-compose run web bundle exec rake db:create
- 投稿日:2019-12-19T15:53:01+09:00
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, :entryattributeを使うと、「このクラスは何をパラメータとして受け取るのか」を明示できるというメリットもあります。なお、この例の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"]以上です。
- 投稿日:2019-12-19T15:50:08+09:00
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, :entryattributeを使うと、「このクラスは何をパラメータとして受け取るのか」を明示できるというメリットもあります。なお、この例の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"]以上です。
- 投稿日:2019-12-19T15:36:28+09:00
○witterもどきのアプリでハッシュタグ機能を実装してみた。
はじめに
どうも、Pirikaraです。
今回は適当に作った○witterもどきのアプリで、ハッシュタグ機能を実装してみたので共有したいと思います。まずはrails new
まずはT○itterもどきのアプリを作ります。
tag機能を実装するので、rails newでtag_appを作ります(適当)tweetモデルとtagモデルを作成しますが、
両者は多対多の関係なので、中間テーブルとしてtweet_tagモデルも作っておきます。tweet.rbclass Tweet < ApplicationRecord has_many :tweet_tags has_many :tags, through: :tweet_tags #写真の投稿も可能にするために、carrierwaveとminimagickを入れておいた mount_uploader :image, ImageUploader endtag.rbclass Tag < ApplicationRecord has_many :tweet_tags has_many :tweets, through: :tweet_tags endtweet_tag.rbclass TweetTag < ApplicationRecord belongs_to :tweet belongs_to :tag endまずはImageとTextのみ投稿ができるTw○tterもどきアプリを作りました。
tweets_controller.rbclass TweetsController < ApplicationController def index @tweets = Tweet.all end def new @tweet = Tweet.new end def create tweet = Tweet.create(tweet_params) redirect_to tweets_path end private def tweet_params params.require(:tweet).permit(:image, :text, :tag_ids) end end現在『Tags』のプルダウンには何も入っていませんが、Tagsテーブルに登録されているものが表示される仕様です。
今回はtextarea内に『#(ハッシュタグ)』があれば、それ以下をtagとして認識、Tagsテーブルに保存される形でコードを書いていきたいと思います。仕様と進め方の確認
- textareaに『#(ハッシュタグ)』があれば『#』以下をtagとして認識し、Tagsテーブルに保存する
- 一覧表示画面にて、tag名が『#○○○』の形で表示される
- 新規投稿画面にて、Tagsのプルダウンにこれまで登録されたTagが表示される。
今回はtweets_controllerのcreateアクションでTweet.createされたあと、
model/tweet.rbでActiveRecordの『after_create』というコールバックを使用し、データベースに保存される直前にTagsテーブルに『#』タグ以下を登録させる感じでやっていきます。
after_create等のコールバックについては、下記のサイトを参考にしました。それではいざ、実装。
Tweet Modelにコールバックの記述
after_create do 〜 endの間に処理を記述していきます。
1. controller側でcreateしたTweetを取得
2. Tweetのtextカラムの値から、『#〇〇〇』に該当する箇所を検出
3. tagsテーブルに保存加えて、view側で『#〇〇〇』をtagsテーブル経由で表示させまます。
4.『#〇〇〇』を含む形でtweetsテーブルに保存されているので、viewで出力する際に正規表現を使って取り除く。
コードは以下の通りです。
models/tweet.rbclass Tweet < ApplicationRecord has_many :tweet_tags has_many :tags, through: :tweet_tags mount_uploader :image, ImageUploader #DBへのコミット直前に実行 after_create do #1.controller側でcreateしたTweetを取得 tweet = Tweet.find_by(id: self.id) #2.正規表現を用いて、Tweetのtext内から『#○○○』の文字列を検出 tags = self.text.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/) #3.mapメソッドでtags配列の要素一つ一つを取り出して、先頭の#を取り除いてDBへ保存する tags.uniq.map do |t| tag = Tag.find_or_create_by(name: t.downcase.delete('#')) tweet.tags << tag end end endtweets/index.html.erb<div class="content"> <% @tweets.each do |tweet| %> <div class="content__post"> <%=image_tag "#{tweet.image}"%> <div class="content__post__tags"> <!--4.gsub()で正規表現に合致する箇所を空欄にする--> <%= simple_format(tweet.text.gsub!(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/, "")) %> <!--4.tweetにタグがあればタグを出力する--> <% if tweet.tags.any? %> <% tweet.tags.each do |tag| %> <p>#<%= tag.name %></p> <% end %> <% end %> </div> </div> <% end %> </div> <div class="footer"> </div>index view側でハッシュタグとともにtweetを表示させることができました。
DBにもちゃんと保存されていますね。
めでたしめでたし。
おわりに
今回はTwitte○もどきのアプリでハッシュタグ機能の実装にチャレンジしてみました。
View側で表示させる時に正規表現を使っているところが不恰好なのでヘルパーに突っ込んでもいいかもしれませんね。応用としては、ハッシュタグからリンクを飛ばして紐づいたtweetを一覧表示できる・・・・・・とかですかね。
ぜひチャレンジしてみてください。おわり。
- 投稿日:2019-12-19T15:28:06+09:00
Protocol Buffers を使って、Rails の RESTful API をスキーマ駆動開発してみた
こんにちは、クラウドワークスの @t0yohei です。普段は Rails や Vue.js などを使って crowdworks.jp の開発をしています。
この記事は クラウドワークス Advent Calendar 2019 の20日目の記事です。昨日は @juntetsu_tei による、Rails更新は最低限Controllerのテストが欲しいというお話でした。
今回は、Protocol Buffers を使った、 Rails の RESTful API はこんな感じに開発できるんじゃない?って内容です。実務で実際に使ったわけではないので、不足している点があると思いますが悪しからず。
用語の整理
Protocol Buffers って?
Google が社内向けに開発を始めたツールです。2008年以降 OSS として公開されており、誰でも閲覧、 contribute することができます。https://github.com/protocolbuffers/protobuf
Protocol Buffers は protobuf と略称して呼ばれており、この記事でも今後 protobuf という呼び方を基本的に使いたいと思います。
GitHub の Overview によると、
Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data.
Protocol Buffers(別名、protobuf)は、構造化データをシリアライズするための Google の言語中立、プラットフォーム中立な拡張可能なメカニズムです。(著者訳)
と書かれていますが、正直難しい。個人的な理解としては、「プロセス間(システム間)でデータをやりとりする際のシリアライズ形式と、シリアライズに必要な各種ライブラリ及びスキーマ定義のためのDSL」という認識です(わかりにくい)。
Protocol Buffers 美しい!一番好きなスキーマ定義言語です!
先ほどの説明の最後に、
スキーマ定義のためのDSLというワードが出てきましたが、この DSL がとても美しく、今でも protobuf が生き残っている由縁と言われています。
サンプルとして、公式ガイドに載っているスキーマ定義を載せておこうと思います。 protobuf のことを知らなくても、なんとなくどんな内容が書かれているのか想像することができるかと思います。message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; }スキーマ定義言語とは何か、なぜそれが重要かといったことは、今さらProtocol Buffersと、手に馴染む道具の話という記事に全てまとめられているので、こちらをご参照いただければと思います。
自分のようにリンク先を読むのが面倒だという人のために、スキーマ定義言語とは何かということについて、自分なりの説明を試みてみようと思います。
スキーマと聞くと、多くの方はDBスキーマを想像するかと思いますが、スキーマ本来の意味としてはDBに限らない「構造」となります。 protobuf で定義するものは、この「構造」の中でも、プロセス間(システム間)でやりとりされるデータの構造(形式・内容)となります。
protobuf と同種のスキーマを定義する言語としては、他にも Open API(旧Swagger) や GraphQL が上げられるかと思います(GraphQLについては、GraphQLのDSLと読み替えた方が正しいかもしれません)。 (注釈1)protobuf は gRPC のスキーマ定義の標準言語に位置付けられており、 gRPC の発達とともに最近再度注目を集めている印象です。
protobuf の詳細について知りたい方は、 Google の protobuf に関する公式ガイドをお読みください。
自分のように、なんやかんや日本語訳が読みやすいよねって方は、上記を翻訳してみたこちらの記事をご参照ください。スキーマ駆動開発
プロセス間(システム間)でやりとりされるデータの構造(スキーマ)を一番最初に決めて、開発を進める手法です。テスト駆動開発がテストを書くことから始まり、テストに駆動された実装になるように、スキーマ駆動開発でもスキーマを先に定義して、スキーマに駆動される開発を行います。
protobuf では、定義したスキーマから実装に使用するコードを自動生成します。この点から、開発がスキーマに駆動されている感覚があり、またスキーマから自動生成されたコードを元に実装を組み上げるため、スキーマ定義と実装間の完全性が保たれます。
オレオレスキーマ定義(ドキュメントにプロセス間でやりとりされるデータをいい感じにまとめる手法)や、 Open API などを使ったスキーマ定義との違いが現れるのがこの部分で、実装とドキュメントが解離しているといった不幸をなくすことができます。 protobuf 素晴らしい。
実装イメージ
前置きが長くなりましたが、 protobuf を使った、Rails の RESTful API がどんな感じになるのかを書いていきます。今回は雰囲気を掴んでもらうことを主眼に置いているので、チュートリアル形式にはなっていません。実際に実装を試したいと思った方は、付録1に簡単なチュートリアルっぽいものを用意しているのでご参照ください。
proto ファイルでのスキーマ定義
まずは proto ファイルでのスキーマ定義です。 今回は、フロントエンドをSPAで実装することを想定した TODO リストにおける Rails の RESTful API のうち、 index, show, create の各アクションについて定義していきます。各アクションではそれぞれ、 Request/Response の形式・内容を定義しています。
task.protosyntax = "proto3"; option ruby_package = "Protos::"; /* GET proto/tasks タスク一覧の取得 */ // message FetchTasksRequest {} message FetchTasksResponse { Tasks tasks = 1; } /* GET proto/task/:id タスク一件の取得 */ // message FetchTaskRequest {} message FetchTaskResponse { Task task = 1; } /* POST proto/tasks タスクの新規追加 */ message CreateTaskRequest { string title = 1; string description = 2; } /* タスクの新規追加成功・失敗時のレスポンス Scuccess Status: 201 Error Status: 400... */ message CreateTaskResponse { Status status = 1; } message Tasks { repeated Task task = 1; } message Task { int32 id = 1; string title = 2; string description = 3; } message Status { int32 code = 1; string message = 2; }細かく見ていきます。まずは
GET proto/tasks タスク一覧の取得の部分。task.proto/* GET proto/tasks タスク一覧の取得 */ // message FetchTasksRequest {} message FetchTasksResponse { Tasks tasks = 1; } message Tasks { repeated Task task = 1; } message Task { int32 id = 1; string title = 2; string description = 3; }この部分では、タスク一覧取得の index アクションについて定義しています。
リクエスト時は、エンドポイントをそのまま叩くだけなので、データの内容を定義しません(定義しないことを明確にするためにコメントアウトしています)。
レスポンスとして、 id, title, description を持つ、 Task 型のデータを複数持つ Tasks 型の値を返します(ややこしい)。次に、
GET proto/task/:id タスク一件の取得の部分。task.proto/* GET proto/task/:id タスク一件の取得 */ // message FetchTaskRequest {} message FetchTaskResponse { Task task = 1; }指定した id を持つタスク一件を取得する show アクションに対する定義です。
リクエスト時は、 id に対するエンドポイントをそのまま叩くだけなので、データの内容を定義しません。 (注釈2)
レスポンスとして、 id, title, description を持つ、 Task 型の値を返します。最後に、
POST proto/tasks タスクの新規追加の部分。task.proto/* POST proto/tasks タスクの新規追加 */ message CreateTaskRequest { string title = 1; string description = 2; } /* タスクの新規追加成功・失敗時のレスポンス Scuccess Status: 201 Error Status: 400... */ message CreateTaskResponse { Status status = 1; } message Status { int32 code = 1; string message = 2; }タスクの新規追加の create アクションの定義です。
リクエストのデータとして、 title と description を送るように定義します。
また、レスポンスとしては、成功・失敗などのステータスを返却するように定義します。スキーマ定義を元に、実装で使用するコードを自動生成
上記で作成した proto ファイルの定義を元に、実際の実装で使用するコードを自動生成します。
$ protoc --ruby_out=./lib proto/task.proto自動生成される内容がこちら。
lib/proto/task_pb.rb# Generated by the protocol buffer compiler. DO NOT EDIT! # source: proto/task.proto require 'google/protobuf' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("proto/task.proto", :syntax => :proto3) do add_message "FetchTasksResponse" do optional :tasks, :message, 1, "Tasks" end add_message "FetchTaskResponse" do optional :task, :message, 1, "Task" end add_message "CreateTaskRequest" do optional :title, :string, 1 optional :description, :string, 2 end add_message "CreateTaskResponse" do optional :status, :message, 1, "Status" end add_message "Tasks" do repeated :task, :message, 1, "Task" end add_message "Task" do optional :id, :int32, 1 optional :title, :string, 2 optional :description, :string, 3 end add_message "Status" do optional :code, :int32, 1 optional :message, :string, 2 end end end module Protos FetchTasksResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTasksResponse").msgclass FetchTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTaskResponse").msgclass CreateTaskRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskRequest").msgclass CreateTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskResponse").msgclass Tasks = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Tasks").msgclass Task = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Task").msgclass Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Status").msgclass endクラス定義わかりにくい...って思うかもしれませんが、その話はここではしない。
自動生成されたコードを使用した各アクションの実装
次に、自動生成されたコードを活用してコントローラーを実装していきます。
app/controllers/proto/tasks_controller.rbclass Proto::TasksController < ApplicationController def index tasks = Task.all tasks_proto = Protos::Tasks.new tasks.each do |each_task| task_proto = Protos::Task.new( id: each_task.id, title: each_task.title, description: each_task.description ) tasks_proto.task.push(task_proto) end response = Protos::FetchTasksResponse.new(tasks: tasks_proto) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data end def show task = Task.find(params[:id]) task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) response = Protos::FetchTaskResponse.new(task: task_proto) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data end def create decoded_data = Protos::CreateTaskRequest.decode(request.raw_post) task = Task.new(title: decoded_data.title, description: decoded_data.description) if task.save status = Protos::Status.new(code: 201, message: "#{task.title}を作成しました。") render plain: build_create_message_encoded(status: status) else status = Protos::Status.new(code: 400, message: "#{task.title}の作成に失敗しました。") render plain: build_create_message_encoded(status: status) end end private def build_create_message_encoded(status:) message = Protos::CreateTaskResponse.new(status: status) Protos::CreateTaskResponse.encode(message) end endこちらも細かく見ていきます。
show
順番が少し前後しますが、まずは簡単な show アクションから。
app/controllers/proto/tasks_controller.rbdef show task = Task.find(params[:id]) task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) response = Protos::FetchTaskResponse.new(task: task_proto) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data end指定された id に合致するタスクを一件取得して、 自動生成で作成された
Protos::Taskクラスのインスタンスオブジェクトにタスクのデータを詰め替えます。
詰め替え後のデータを元に、レスポンスオブジェクトを作成し、encodeメソッドでレスポンスオブジェクトをシリアライズします。そして最後に、シリアライズされたデータを返却します。
せっかくなので、各部分でデータがどう変化していくかをコメントに書いてみました。task = Task.create(id: 1, title: 'title', description: 'description') # => #<Task id: 1, title: "title", created_at: "2019-12-17 08:30:09", updated_at: "2019-12-17 08:30:09", description: "description"> task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) # => <Protos::Task: id: 1, title: "title", description: "description"> response = Protos::FetchTaskResponse.new(task: task_proto) # => <Protos::FetchTaskResponse: task: <Protos::Task: id: 1, title: "title", description: "description">> response_encoded_data = Protos::FetchTaskResponse.encode(response) # => "\n\x16\b\x01\x12\x05title\x1A\vdescription"最終的には、
"\n\x16\b\x01\x12\x05title\x1A\vdescription"のような文字列がレスポンスとして返されることになります。
このデータの呼び方は特に定義されていないのですが、 (protobuf の)シリアライズデータと呼ぶことにします。
このシリアライズデータには、型情報や実際の値が含まれているので、同じ proto ファイルのスキーマ定義を元に自動生成されたクラス定義(多言語でも可)を使用することで、受け取り先で decode することができます。もちろん、 encode したシリアライズデータをそのまま decode することもできます(実際の開発でやることは少ないと思いますが)。
response_encoded_data = Protos::FetchTaskResponse.encode(response) decoded_data = Protos::FetchTaskResponse.decode(response_encoded_data) # =><Protos::FetchTaskResponse: task: <Protos::Task: id: 1, title: "title", description: "description">>また、それでも json が好きだ!という場合は、 json に変換することも可能です。
response = Protos::FetchTaskResponse.new(task: task_proto) Protos::FetchTaskResponse.encode_json(response) # => "{\"task\":{\"id\":1,\"title\":\"title\",\"description\":\"description\"}}"index
次に index アクションについて見ていきます。
app/controllers/proto/tasks_controller.rbdef index tasks = Task.all tasks_proto = Protos::Tasks.new tasks.each do |each_task| task_proto = Protos::Task.new( id: each_task.id, title: each_task.title, description: each_task.description ) tasks_proto.task.push(task_proto) end response = Protos::FetchTasksResponse.new(tasks: tasks_proto) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data endshow アクションとの違いは特にないのですが、データの詰め替えの部分の処理が長ったらしくなってしまっています... この辺は要リファクタリングです。
create
最後に、 create アクションです。
app/controllers/proto/tasks_controller.rbdef create decoded_data = Protos::CreateTaskRequest.decode(request.raw_post) task = Task.new(title: decoded_data.title, description: decoded_data.description) if task.save status = Protos::Status.new(code: 201, message: "#{task.title}を作成しました。") render plain: build_create_message_encoded(status: status) else status = Protos::Status.new(code: 400, message: "#{task.title}の作成に失敗しました。") render plain: build_create_message_encoded(status: status) end end private def build_create_message_encoded(status:) message = Protos::CreateTaskResponse.new(status: status) Protos::CreateTaskResponse.encode(message) end
request.raw_postで post で送られてくる(protobuf の)シリアライズデータを取得して、デコードします。デコード後は通常の create 処理と同様に save してあげて、レスポンスデータを show メソッドなどと同様の方法で作成し、レスポンスを返してあげます。まとめ
ひとまず、 proto ファイルでスキーマを定義してからレスポンスを返す実装を一通り紹介できたかなと思います。最後に、 protobuf を使ったスキーマ駆動開発って何がいいの?または何が悪いの?って点について個人的な感想を書きたいと思います。
良い点
- 簡素で明瞭なスキーマ定義が書ける。
- 自動生成するコードを元に実装を進めるので、スキーマ定義と実装が基本的に一致する。
- スキーマを先に決めて開発するので、フロントエンド・バックエンドなどの実装で認識の齟齬が起きにくい。
- スキーマ定義とそれを元に自動生成されるコードがあるので、バックエンドの実装を待たずに、フロントエンドでモックを作ったりテストを書いたりできる。
悪い点
- 自動生成されるコードに依存する実装になるので、やめたい時に引き剥がずのが一手間かかる。
- protobuf のドキュメントや実装サンプルが豊富ではないので、学習がちょっとやりにくい。
実際に業務に適用できるかどうかは、チームや組織の状況によりそうです。個人的には JSON や YAML を使ったスキーマ定義よりも protobuf の方が見やすいと思うので、スキーマ駆動開発をする際は是非とも使いたいという気持ちです。
ひとまず本編は以上になります。最後まで読んでいただきありがとうございました。
付録1 チュートリアルのようなもの
この付録1では、実際に protobuf を使った rails の RESTful API を作成していく手順について書いていこうと思います。
本編と同様に、フロントエンドをSPAで実装することを想定した TODO リストを想定して開発を進めていきます。railsの基本的な環境構築は省略しますが、自分は下記の手順で構築しましたので必要に応じて参考にしてください。
https://qiita.com/t0yohei/items/9f3a418d3ea61e090be3また、実装全体やテスト、 Nuxt.js の SPA への適用法などの詳細が知りたい場合は、下記のリポジトリを参照いただければと思います。
https://github.com/t0yohei/rails-nuxt-protobuf-todoでは、早速進めていきましょう。
環境構築
今回は下記の環境で動作することを確認しています。
ruby: 2.6.4 rails: 6.0.0protoc のインストール
proto ファイルから実装コードを自動生成する際に、
protocというライブラリが必要になるのでインストールします。$ brew install protobufprotobuf 用の gem のインストール
今回は、 google に開発が主導されている google-protobuf という gem を使用します。
gem の選定に関しては、付録2 gem の選定の章で話そうと思います。Gemfile に
gem 'google-protobuf'を追記して、bundle installを実行してください。model, migration ファイルの作成
まずは model, migration ファイルを作成して、タイトルと詳細だけを保持するシンプルなタスクテーブルを作成します。
$ bundle exec rails g model Task title:string description:stringvalidation の追加
app/models/task.rbclass Task < ApplicationRecord validates :title, presence: true, length: { in: 1..25 } validates :description, length: { maximum: 100 } end各種制約の追加
db/migrate/20190000000000_create_tasks.rbclass CreateTasks < ActiveRecord::Migration[6.0] def change create_table :tasks do |t| t.string :title, :string, null: false, default: '', limit: 25 t.string :description, :string, limit: 100 t.timestamps end end endmigration を実行しましょう。
$ bundle exec rails db:migrate続いて、開発がしやすいように seed データを準備します。
db/seeds.rbTask.create(title: '大掃除', description: '年末なので気だるいけど大掃除をする') Task.create(title: 'advent calendar を書く', description: 'そろそろ書き始めないとヤバイ')忘れずデータを投入しておきましょう。
$ bundle exec rails db:seedproto ファイルでの スキーマの定義
次に、システム間でどういうデータ形式でデータをやりとりするのかを、 proto ファイルを使用して定義します。
今回は RESTful な API なので、 Request/Response をそれぞれ定義しています。proto/task.proto/* command: protoc --ruby_out=./lib proto/task.proto */ syntax = "proto3"; option ruby_package = "Protos::"; /* GET proto/tasks タスク一覧の取得 */ // message FetchTasksRequest {} message FetchTasksResponse { Tasks tasks = 1; } /* GET proto/task/:id タスク一件の取得 */ // message FetchTaskRequest {} message FetchTaskResponse { Task task = 1; } /* POST proto/tasks タスクの新規追加 */ message CreateTaskRequest { string title = 1; string description = 2; } /* タスクの新規追加成功・失敗時のレスポンス Scuccess Status: 201 Error Status: 400... */ message CreateTaskResponse { Status status = 1; } message Tasks { repeated Task task = 1; } message Task { int32 id = 1; string title = 2; string description = 3; } message Status { int32 code = 1; string message = 2; }
option ruby_package = "Protos::";の部分で、自動生成するクラスのモジュールを定義しています。こうしておくことで、 ActiveRecord の Task クラスとの名前衝突を回避することができます。続いて、実装で使用するコードの自動生成します。
$ protoc --ruby_out=./lib proto/task.proto下記のようなコードが生成されているかと思います。
lib/proto/task_pb.rb# Generated by the protocol buffer compiler. DO NOT EDIT! # source: proto/task.proto require 'google/protobuf' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("proto/task.proto", :syntax => :proto3) do add_message "FetchTasksResponse" do optional :tasks, :message, 1, "Tasks" end add_message "FetchTaskResponse" do optional :task, :message, 1, "Task" end add_message "CreateTaskRequest" do optional :title, :string, 1 optional :description, :string, 2 end add_message "CreateTaskResponse" do optional :status, :message, 1, "Status" end add_message "Tasks" do repeated :task, :message, 1, "Task" end add_message "Task" do optional :id, :int32, 1 optional :title, :string, 2 optional :description, :string, 3 end add_message "Status" do optional :code, :int32, 1 optional :message, :string, 2 end end end module Protos FetchTasksResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTasksResponse").msgclass FetchTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("FetchTaskResponse").msgclass CreateTaskRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskRequest").msgclass CreateTaskResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("CreateTaskResponse").msgclass Tasks = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Tasks").msgclass Task = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Task").msgclass Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("Status").msgclass endlib 配下に 自動生成されるファイルを置くようにしたので、下記を追記してちゃんと読み込まれるようにします。
config/application.rbDir["#{Rails.root}/lib/proto/*.rb"].each { |file| require file }route.rb で API のエンドポイントを定義
route.rbRails.application.routes.draw do namespace :proto do resources :tasks end endcontroller の実装
app/controllers/proto/tasks_controller.rbclass Proto::TasksController < ApplicationController def index tasks = Task.all tasks_proto = Protos::Tasks.new tasks.each do |each_task| task_proto = Protos::Task.new( id: each_task.id, title: each_task.title, description: each_task.description ) tasks_proto.task.push(task_proto) end response = Protos::FetchTasksResponse.new(tasks: tasks_proto) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data end def show task = Task.find(params[:id]) task_proto = Protos::Task.new( id: task.id, title: task.title, description: task.description ) response = Protos::FetchTaskResponse.new(task: task_proto) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data end def create decoded_data = Protos::CreateTaskRequest.decode(request.raw_post) task = Task.new(title: decoded_data.title, description: decoded_data.description) if task.save status = Protos::Status.new(code: 201, message: "#{task.title}を作成しました。") render plain: build_create_message_encoded(status: status) else status = Protos::Status.new(code: 400, message: "#{task.title}の作成に失敗しました。") render plain: build_create_message_encoded(status: status) end end private def build_create_message_encoded(status:) message = Protos::CreateTaskResponse.new(status: status) Protos::CreateTaskResponse.encode(message) end endちゃんと動くか動作確認をします。
$ bundle exec rails s
http://localhost:3000/proto/tasksを見てみると、文字化けしたものが出力されていると思います。(自分の環境ではゴミデータも混ざっているので、写真の通りには出てこないと思いますが悪しからず)テストの作成
長くなりそうだったので、 index アクションのテストだけを記載しました。他のアクションのテストについては、こちらのリポジトリをご確認ください。
spec/requests/proto/tasks_spec.rbrequire 'rails_helper' RSpec.describe "Proto::Tasks", type: :request do describe "GET /proto/tasks" do subject { get proto_tasks_path } it "特に条件を指定しなくても、200を返すこと" do subject expect(response).to have_http_status(200) end context "taskが1件も存在しないとき" do it "空の encoded_data が返却されること" do subject expect(response).to have_http_status(200) decoded_response = Protos::FetchTasksResponse.decode(response.body) expect(decoded_response.tasks.task).to be_empty end end context "taskが1件存在するとき" do let!(:task) { Task.create(id: 1, title: 'title', description: 'description') } it "task1件分の encoded_data が返却されること" do subject decoded_response = Protos::FetchTasksResponse.decode(response.body) expect(decoded_response.tasks.task.count).to eq(1) expect(decoded_response.tasks.task.first.id).to eq(task.id) expect(decoded_response.tasks.task.first.title).to eq(task.title) expect(decoded_response.tasks.task.first.description).to eq(task.description) end end context "taskが2件存在するとき" do let!(:task1) { Task.create(id: 1, title: 'title1', description: 'description1') } let!(:task2) { Task.create(id: 2, title: 'title2', description: 'description2') } it "task2件分の encoded_data が返却されること" do subject decoded_response = Protos::FetchTasksResponse.decode(response.body) expect(decoded_response.tasks.task.count).to eq(2) expect(decoded_response.tasks.task.first.id).to eq(task1.id) expect(decoded_response.tasks.task.first.title).to eq(task1.title) expect(decoded_response.tasks.task.first.description).to eq(task1.description) end end end endindex, show メソッドをリファクタリング
index アクションにコードが長々とあるのでリファクタリングをします。
app/controllers/proto/tasks_controller.rbdef index response = Protos::FetchTasksResponse.new(tasks: Task.convert_all_to_message_object) response_encoded_data = Protos::FetchTasksResponse.encode(response) render plain: response_encoded_data, status: :ok end def show task = Task.find(params[:id]) response = Protos::FetchTaskResponse.new(task: task.convert_to_message_object) response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data, status: :ok endapp/models/task.rbclass Task < ApplicationRecord def self.convert_all_to_message_object Protos::Tasks.new( task: Task.all.map { |task| task.convert_to_message_object } ) end def convert_to_message_object Protos::Task.new( id: self.id, title: self.title, description: self.description ) end endリファクタリングについては、こちらの記事を参考にさせてもらいました。
Building APIs with Rails + Protocol Buffersこれで、一通りの実装が作成できたかと思います。お疲れ様でした。
update, destroy アクションの実装が気になる場合は、こちらをご覧ください。
https://github.com/t0yohei/rails-nuxt-protobuf-todo/blob/master/backend/app/controllers/proto/tasks_controller.rb付録2 gem の選定
今回の実装では、 google が公式に出している、 google-protobuf を使用しました。
ruby で protobuf を使用する際に導入できそうな gem としては他にも、 protobuf などがあります。(ややこしいですがgoogleの公式の方に
後者の gem も今でもメンテナンスされており、使用しても問題ないように思います。
(なぜ複数の active な gem が存在しているのかということはちゃんとは調べていないのですが、公式の protobuf の ruby ライブラリ(gem)より先に、有志の gem が作られたのかなー?と勝手に予想しています。現に proto2 の時点では ruby で protobuf を扱うことができなかったようです(現在は proto3 がデフォルトなので1つ前のバージョンの時)。)どちらの gem を選択するかは難しいところですが、自分が知っている限りでのそれぞれの特徴を書いておこうと思います。
google-protobuf
リポジトリ: google-protobuf
- google が公開しているものなので、ちゃんとメンテナンスされ続けそう。
- gem の実装に Java とか C などのコードが使われている。
- 自動生成される ruby のコードがわかりにくい。
protobuf
リポジトリ: protobuf
- メンテナンスは今でもされていそう(2019年12月時点)。
- ruby で実装されている(重要)。
- 自動生成される ruby のコードがシンプルでわかりやすい。
- protobuf-activerecord を使う際に必須になる。
まとめ
個人で趣味として使うのであれば protobuf の方が、使いやすそうだなーと個人的に思っています。
protobuf-activerecordを使って、実装をシンプルにできそうだし。業務で使う場合にどちらを使用するかは、皆さんでご判断ください。付録3 例外処理
protobuf で自動生成されたコードを使って実装を進める場合、レスポンスは proto ファイルで定義した response の型で返してあげる必要があります。そのため、
ApplicationControllerに全メソッド共通の例外処理などを簡単には書くことはできません。そのため、個人的には下記のような形式がまだ綺麗な方じゃないかなーと思っています。app/controllers/proto/tasks_controller.rbdef show task = Task.find(params[:id]) response = Protos::FetchTaskResponse.new(task: task.convert_to_message_object) rescue => e response = Protos::Status.new(code: 500, message: e.message) ensure response_encoded_data = Protos::FetchTaskResponse.encode(response) render plain: response_encoded_data, status: :ok end注釈
注釈1
protoc-gen-swagger というライブラリを介して、 protobuf の定義から OpenAPI 定義ファイルを生成することも可能なようです。
注釈2
show アクションのリクエスト関しては、
/* GET proto/tasks タスク一件の取得 */ message FetchTaskRequest { int32 id = 1; }と表現することもできるのですが、rails の show アクションのデフォルト定義に合わせて、今回は
GET proto/task/:idで取得することにしました。参考文献など
今さらProtocol Buffersと、手に馴染む道具の話
WEB+DB PRESS Vol.108 スキーマ駆動Web API開発
Web API に秩序を与える Protocol Buffers
Protocol Buffers | Google Developers
- 投稿日:2019-12-19T13:36:09+09:00
rails データ保存あれこれ
基本
new → view → controller
- 通常
def product_params params.require(:product).permit(:name, :desc) endfields_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参考記事
- 投稿日:2019-12-19T12:06:45+09:00
Capistranoで自動デプロイ時に起きるエラーについて〜AWS〜
はじめに
Capistranoを使って、AWSに自動デプロイをかけた時に出るエラーのお話
本件は主に再起動時に起きるエラーです
最後にデプロイ時に確認する事項をまとめていますので、そちらも軽くご参照いただければと思いますErroe内容
bundle exec cap production deploy実行後
terminalMac_ikuy:"hogehoge" User$ bundle exec cap production deploy #<Thread:0x00007f8b2b50bb18@/Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:10 run> terminated with exception (report_on_exception is true): Traceback (most recent call last): 1: from /Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:11:in `block (2 levels) in execute' /Users/mac_ikuy/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.20.0/lib/sshkit/runners/parallel.rb:15:in `rescue in block (2 levels) in execute': Exception while executing as ec2-user@18.177.78.202: Authentication failed for user ec2-user@xx.xxx.xxx.xxx (SSHKit::Runner::ExecuteError) (Backtrace restricted to imported tasks) cap aborted! SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@xx.xxx.xxx.xxx: Authentication failed for user ec2-user@xx.xxx.xxx.xxx Caused by: Net::SSH::AuthenticationFailed: Authentication failed for user ec2-user@xx.xxx.xxx.xxx Tasks: TOP => rbenv:validate (See full trace by running task with --trace)こんな感じのエラーが出ます
今回の話の結論からいうと、SSHキーが消えてしまっているので、.sshにて下記コマンドを入力しますterminal$ cd ~/.ssh .ssh $ ssh-add ~/.ssh/hoge.pemhoge.pemにはインスタンスに対応したpemキーを入力してください
Error解決しないときは
エラー文に慣れない方はなんかいっぱい書いてあってうわぁって思うかもしれませんが、ポイントは"Caused by"です
これのせいで、ってことですね
"Exception while executing"を直訳すると「実行中の例外」です
何か例外が起きてしまったんだなってことですね
AWSへのデプロイ時の基本注意点としては、
- mysql/nginexの再起動
- unicorn killの確認
です
前者は[ec2-user@ip-xxx ~]$ sudo service nginx restart [ec2-user@ip-xxx ~]$ sudo service mysqld restart後者は
[ec2-user@ip-xxx ~]$ ps aux | grep unicorn ec2-user 4223 0.0 0.0 11xxxx master ec2-user 4175 0.0 0.0 11xxxx ~~~~省略~~~~ ec2-user 4236 0.0 0.0 11xxxx ~~~~省略~~~~ [ec2-user@ip-xxx ~]$ kill 4223 #masterのNO.上記ができているにも関わらずデプロイできないときは、インスタンスを一度停止しましょう
"停止"が完了したら"開始"をしましょう
大概のエラーはこれでで解決できます。まとめ
解決方法のまとめです
- "Net::SSH::AuthenticationFailed: Authentication failed"が出たときはsshキーが消えている可能性があるので、addしてやる
- mysql/nginexの再起動
- unicornのkill
- インスタンスの再起動
- 投稿日:2019-12-19T07:21:37+09:00
letを使った遅延処理、shared_examples
letを使った遅延処理
spec/system/tasks_spec.rbrequire '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.rbrequire '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のファイルを作ってそこに定義した方が良さそう。
- 投稿日:2019-12-19T06:17:49+09:00
VSCodeの拡張機能でRailsと仲良くなる
はじめに
こちらは Classi Advent Calendar 19日目の記事です。
こんにちは、Classi Webエンジニアの@hakshuです。皆さんはRailsを書く時エディタは何を使っていますか?
RubyMine?Vim?Emacs?VSCode?Atom?さくらエディタ?自分はフロント/サーバー関わらず、JavaScriptやTypeScriptなどを書くことが多かったので、Visual Studio Code、いわゆるVSCodeを愛用しています。
現在のプロジェクトでは、フロントエンド(Angular)/サーバーサイド(Rails)のどちらも書いており、いちいちエディタを切り替えるのも面倒で、使い慣れていることもあるので、VSCodeを使っています。
TypeScriptもVSCodeもMicrosoft製ということもありますし、静的型付けのため、補完はとても効くので、サクサク書いていくことができます。
ただ、Rubyについては、動的型付けのため、補完をいい感じに効かせるには拡張機能などでカバーが必要です。
なので、今回は自分がMacのVSCodeに入れている拡張機能やその設定の紹介をしていこうと思います。
拡張機能
Ruby・Rails
Ruby
Ruby言語サポートです。Rubyのコードを書き始める前にこれを入れましょう。
文法の最低限のインテリセンスが有効になります。
他にもrbenvやasdfなどでのrubyバージョン切り替えにVSCodeを対応させたり、RubyCopなどでのLint,Formatに対応したりしています。
Settings.json 設定例
"ruby.useBundler": true, // VSCodeが実行するgem関連のコマンドをbundle execで実行するかどうか "ruby.lint": { "rubocop": { // RuboCop, Standard or Reek "useBundler": true // bundlerでインストールしているgemを使うか否か }, }, "ruby.format": "rubocop" // formatに使うgemを指定(RuboCop, Standard, Rufo or RubyFMT)。bundlerでインストールしたgemを使うかどうかは、ruby.useBundler依存
VSCode Ruby
上記拡張機能「Ruby」を入れると自動でインストールされます。ruby・erbのシンタックスハイライトが有効になります。
Rails
その名の通りRailsサポートの拡張機能です。
assetやタグのsnippetを提供しています。
また、
Opt + .で関連ファイルにナビゲーションしてくれます。
他にもRailsのビルトインオブジェクトにフォーカスを当てて、Opt + F1でRailsのドキュメントを確認することや、自作クラスの定義元に飛ぶことが可能になります。
Ruby Solargraph
上記Rails拡張機能を入れるとインストールされます。Rubyのインテリセンスを強化できます。
設定例
Solargraphを使えるようにするには主に3つの方法があります。
- gemをglobal installする
- Gemfileに追加し、bundlerでinstallする
- docker containerなど外部サーバーとして動かす
1. gemをglobal installする場合
solargraphをインストールします。
$ gem install solargraph # グローバルなgemのdocumentを生成します。 $ yard gemssettings.jsonは以下のように設定しましょう。
"solargraph.useBundler": false2. bundlerでinstallする場合
Gemfileにsolargraphを追加してインストールします。
gem 'solargraph', group: :development$ bundle install # プロジェクトにインストールしているgemのdocumentを生成します $ bundle exec yard gemssettings.jsonは以下のように設定しましょう。
"solargraph.useBundler": true1と2の共通設定
solargraphの設定ファイルを作成します。
設定内容については以下に解説があるので、プロジェクトに合わせて設定しましょう。
Configuration$ solargraph config .Solargraph0.34.0以降で、Railsの補完を効かせるには、以下コマンドをRailsプロジェクトのルートディレクトリで実行しましょう。
ActiveRecordなど、Railsのコア部分のgemはSolargraphが使用しているYARDではなく、RDocを使用しているため、このコマンドでRDocからYARDへの変換をしています。$ solargraph bundleその後こちらのファイルをプロジェクトに追加しましょう。
Solargraphが静的解析でRailsアプリを解析しきれない部分とのギャップを埋めるために必要となります。
参考ドキュメント※Solargraph0.34.0以前の場合は
.solargraph.ymlのrequireに以下を追加する必要があります。solargraph.ymlrequire: - actioncable - actionmailer - actionpack - actionview - activejob - activemodel - activerecord - activestorage - activesupport3. Dockerコンテナなど外部サーバーとして立ち上げる場合
まずは以下のようなSolargraphを起動するためのDockerfileをプロジェクトのルートに追加しましょう。
FROM ruby:2.6.4-alpine3.10 RUN apk update \ && apk add --update make gcc g++ # Install gems WORKDIR /home/solargraph COPY Gemfile Gemfile.lock ./ RUN gem install bundler RUN bundle install --jobs=4 RUN gem install solargraph yard # Generate gem documentation RUN yard gems # Convert rails documentation RUN solargraph bundle # Solargraph server port EXPOSE 7658 # Run Solargraph CMD [ "solargraph", "socket", "--host=0.0.0.0", "--port=7658" ]このDockerfileでimageを作成し、containerを起動します。
# build image $ docker build -t vscode-solargraph:latest . # run container $ docker run -d --rm -p 8091:7658 --name solargraph vscode-solargraphこちらのcontainerを使うようsettings.jsonを次のように設定します。
設定後、VSCodeを再起動すると、solargraphが使えるようになります。"solargraph.transport": "external", "solargraph.externalServer": { "host": "localhost", "port": 8091 }
Ruby on Rails
Railsのスニペットを提供してくれます。
solargraphのインテリセンスだけでは、RailsのAPI補完が不十分なので、こちらで補っています。
Rails DB Schema
DB Schemaに沿って、モデルのカラム名を補完してくれます。
Rails Go to Spec
Cmd + shift + yで実装ファイルとテストファイルを行き来できます。endwise
endを自動挿入してくれます。
Rainbow End
endを色分けする拡張機能です。
do~endがネストする時に見分けにくかったので、入れています。
その他Ruby・Railsに限らない拡張機能
indent-rainbow
インデントに応じて色分けしてくれます。
Material Icon Theme
エクスプローラー内のフォルダやファイルアイコンなどが見やすくなるテーマです。
Swagger Viewer
VSCode上でSwaggerファイルのプレビューが可能になります。最後に
以上、拡張機能とその設定でした。
ぜひ気になったものがあったら使ってみてください。明日は @yosuke_ohara さんです。お楽しみに!
- 投稿日:2019-12-19T04:40:38+09:00
特定のview(url)でグローバル変数を定義
こんにちは!
この前個人アプリを作成しているときにjs(jquery)とgem 'gon'について少しハマったのでご紹介しようと思います
なににハマったかと言うと、特定のビューの中で使用するjsの変数をグローバル変数として指定したく、jsのグローバル変数にgem'gon'で指定した値を代入したところ、アクションの外でエラーが出てしまい、他のjsファイルの実行の妨げになってしまったのです(ちょっと何言ってるかわかりづらくてすみません)
もう詳しい方ならこの一文を見ただけで察しはついているのではと思いますが、この時の状況と解決を簡単にご紹介します
'gon'とは
railsのコントローラで定義した変数をjs内で使用できるようにしてくれるgemです
https://qiita.com/s_nakamura/items/5d153f7d9db1b1190296グローバル変数とは
〇〇.jsvar test_global = hogehoge $(function() { //処理 });このようにjsの関数の外に変数を定義すると、どこでも使える変数を定義することができます(グローバル変数)
また、関数の中であっても'var'など付けず、変数名のみで変数を指定しても、グローバル変数として定義することができます起こったこと
〇〇.jsvar test_gon = gon.test_gon $(function() { //この中で上記のグローバル変数を使いたい });私はこんな感じでグローバル変数を定義しました
すると、このjsファイルが実行されるビューにいるとき(gonで変数を指定しているアクションを実行したとき)は問題ないのですが、それ以外のビューにいる場合エラーになってしまいますよく考えればわかることですが、アクション外では当然gon.test_gonは定義されていないのでエラーとなるのです
えぇ・・といってもグローバル変数にしないとやりたいことが実装できない・・
と考え、ググっていたところいい条件分岐のやり方がありました解決
〇〇.jsif(document.URL.match("/urlの文字列")) { //指定する文字列がURLに含まれる場合に実行する内容 var test_gon = gon.test_gon; }こんな感じで指定してあげると、特定のビューで使用するグローバル変数を定義してあげることができます
ちなみに
〇〇.jsif(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てなんでもできちゃうのね
ともあれ指定できてよかったです
- 投稿日:2019-12-19T04:40:38+09:00
(rails)特定のview(url)でグローバル変数を定義
こんにちは!
この前個人アプリを作成しているときにjs(jquery)とgem 'gon'について少しハマったのでご紹介しようと思います
なににハマったかと言うと、特定のビューの中で使用するjsの変数をグローバル変数として指定したく、jsのグローバル変数にgem'gon'で指定した値を代入したところ、アクションの外でエラーが出てしまい、他のjsファイルの実行の妨げになってしまったのです(ちょっと何言ってるかわかりづらくてすみません)
もう詳しい方ならこの一文を見ただけで察しはついているのではと思いますが、この時の状況と解決を簡単にご紹介します
'gon'とは
railsのコントローラで定義した変数をjs内で使用できるようにしてくれるgemです
https://qiita.com/s_nakamura/items/5d153f7d9db1b1190296グローバル変数とは
〇〇.jsvar test_global = hogehoge $(function() { //処理 });このようにjsの関数の外に変数を定義すると、どこでも使える変数を定義することができます(グローバル変数)
また、関数の中であっても'var'など付けず、変数名のみで変数を指定しても、グローバル変数として定義することができます起こったこと
〇〇.jsvar test_gon = gon.test_gon $(function() { //この中で上記のグローバル変数を使いたい });私はこんな感じでグローバル変数を定義しました
すると、このjsファイルが実行されるビューにいるとき(gonで変数を指定しているアクションを実行したとき)は問題ないのですが、それ以外のビューにいる場合エラーになってしまいますよく考えればわかることですが、アクション外では当然gon.test_gonは定義されていないのでエラーとなるのです
えぇ・・といってもグローバル変数にしないとやりたいことが実装できない・・
と考え、ググっていたところいい条件分岐のやり方がありました解決
〇〇.jsif(document.URL.match("/urlの文字列")) { //指定する文字列がURLに含まれる場合に実行する内容 var test_gon = gon.test_gon; }こんな感じで指定してあげると、特定のビューで使用するグローバル変数を定義してあげることができます
ちなみに
〇〇.jsif(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てなんでもできちゃうのね
ともあれ指定できてよかったです
- 投稿日:2019-12-19T02:08:34+09:00
画像の複数投稿??プレビュー表示??え??
〇〇エキスパートにて
どうも、pirikaraです。
チーム開発にて某メルカリのクローンサイトを作成中、商品出品の画像投稿で詰まりました。
プレビューは表示されるのにデータが入ってない・・・・・・
プレビュー消したのにデータが残っている・・・・・・
など散々格闘したので、参考になればと思って書きました。10月からプログラミング学習を開始した弱々エンジニアですので、お手柔らかによろしくお願い致します。
仕様
- 画像を10枚投稿できる
- 投稿した画像は1枚ずつプレビューされる
- 5枚目以降は2段目にプレビュー表示される
- 削除を押すとプレビューから消える
- 追加・削除したプレビューとfile_fieldの中身が同期している
やってみよう
まずはrails newで適当なアプリケーションを立ち上げます。
今回は適当にsample_appとしました。
databaseはmySQLを使用します。今回は商品出品なので、ItemモデルとImageモデルを作成。
CarrierWaveとminimagickのgemをインストールして、
アソシエーションを組みます。item.rbclass Item < ApplicationRecord has_many :images, dependent: :destroy accepts_nested_attributes_for :images, allow_destroy: true endimage.rbclass Image < ApplicationRecord belongs_to :animal, optional: true mount_uploader :image_url, ImageUploader end※accepts_nested_attributes_forは以下の記事を参考にしました
Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for)databaseのカラムを作成します。
今回はitemはnameのみ、imageはitem_idでitemと紐付けて、image_urlカラムに画像ファイル名が保存されるようにしました。
フォームを作るよ
できました。
items/new.haml.main %section.main__block = form_with model:@item, local:true do |f| %h2.sell__block__head 商品の情報を入力 .sell__block__form .sell__block__form__upload %h3.sell__block__form__upload__head 出品画像 %span.require 必須 %p 最大10枚までアップロードできます #image-box-1 .item-num-0#image-box__container = f.fields_for :images do |i| .input-area = i.file_field :image, type: 'file', name: "item[images_attributes][][image_url]", value:"", style: "display:none", id:"img-file" %label{for: "img-file"} %i.fas.fa-camera .sell__block__form__name .form-group__name %label 商品名 %span.require 必須 %div = f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name" .sell__block__form__btn %div = f.submit "出品する",class: "btn-default__btn-red"コードはこんな感じ。
items/new.haml#image-box-1 .item-num-0#image-box__containerこの間にプレビューのコードがJavaScriptで挿入される感じで実装していきます。
JQueryを書くよ
まず、作業を以下の4つに分けました。
1. プレビューの表示
2. プレビューの削除
3. プレビューの複数表示
4. ドラッグ&ドロップへの対応順番に作業していきます。
1. プレビューの表示
できました。
コードは以下の通りです。
new_item.js$(function(){ //querySelectorでfile_fieldを取得 var file_field = document.querySelector('input[type=file]') //fileが選択された時に発火するイベント $('#img-file').change(function(){ //選択したfileのオブジェクトをpropで取得 var file = $('input[type="file"]').prop('files')[0]; //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む var fileReader = new FileReader(); //読み込みが完了すると、srcにfileのURLを格納 fileReader.onloadend = function() { var src = fileReader.result var html= `<img src="${src}" width="114" height="80">` //image_box__container要素の前にhtmlを差し込む $('#image-box__container').before(html); } fileReader.readAsDataURL(file); }); });今の状態ではプレビューが削除できないため、削除できるようにします。
2. プレビューの削除
まずはhtml部に削除ボタンを追加します。
new_item.jsvar html= `<div class='item-image' data-image="${file.name}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="114" height="80" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--delete'>削除</div> </div> </div>`削除ボタンを押すと発火するイベントを作成し、プレビューが削除できるようにします。
new_item.js$(document).on("click", '.item-image__operetion--delete', function(){ //プレビュー要素を取得 var target_image = $(this).parent().parent() //プレビューを削除 target_image.remove(); //inputタグに入ったファイルを削除 file_field.val("") })file_fieldに画像が入ったと同時にプレビューが出て、プレビュー削除と同時にfile_fieldに入った画像が消えました。
3. プレビューの複数表示
ここからが本番です。
大まかな流れとしては、
①DataTransferオブジェクトを作成し、データを格納する箱とする
②each文を用いて、DataTransferオブジェクトの箱にfileを追加していく。
③箱の中身をfile_fieldに代入して、複数データを持たせる
④ついでeach文の中でプレビューを追加していく。
って感じです。DataTransferオブジェクトに関しては、下記のサイトを参考にしました。
Javascriptプログラミング講座画像を挿入すると、DataTransferオブジェクト内のfiles: FileListにデータが追加されていきます。
それではコードを書いていきましょう。
new_item.js$(function(){ //DataTransferオブジェクトで、データを格納する箱を作る var dataBox = new DataTransfer(); //querySelectorでfile_fieldを取得 var file_field = document.querySelector('input[type=file]') //fileが選択された時に発火するイベント $('#img-file').change(function(){ //選択したfileのオブジェクトをpropで取得 var files = $('input[type="file"]').prop('files')[0]; $.each(this.files, function(i, file){ //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む var fileReader = new FileReader(); //DataTransferオブジェクトに対して、fileを追加 dataBox.items.add(file) //DataTransferオブジェクトに入ったfile一覧をfile_fieldの中に代入 file_field.files = dataBox.files var num = $('.item-image').length + 1 + i fileReader.readAsDataURL(file); //画像が10枚になったら超えたらドロップボックスを削除する if (num == 10){ $('#image-box__container').css('display', 'none') } //読み込みが完了すると、srcにfileのURLを格納 fileReader.onloadend = function() { var src = fileReader.result var html= `<div class='item-image' data-image="${file.name}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="114" height="80" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--delete'>削除</div> </div> </div>` //image_box__container要素の前にhtmlを差し込む $('#image-box__container').before(html); }; //image-box__containerのクラスを変更し、CSSでドロップボックスの大きさを変えてやる。 $('#image-box__container').attr('class', `item-num-${num}`) }); }); //削除ボタンをクリックすると発火するイベント $(document).on("click", '.item-image__operetion--delete', function(){ //プレビュー要素を取得 var target_image = $(this).parent().parent() //プレビューを削除 target_image.remove(); //inputタグに入ったファイルを削除 file_field.val("") }) });image-box__containerクラスを持つdivタグのクラスをプレビューBOXの数に応じて変更し、
CSSをによってドロップボックスが小さくなるようにしました。
画像が5枚になったら、cssの指定でドロップボックスのwidthを100%に戻します。
するとドロップボックスが2行目に落ちてきます。
10枚になったらドロップボックスを消してあげます。items/new.haml.item-num-0#image-box__containeritem_new.scss.item-num-0{ width: 100%; } .item-num-1{ width: 491px; } .item-num-2{ width: 363px; } .item-num-3{ width: 234px; } .item-num-4{ width: 106px; } //下段のfile_field .item-num-5{ width: 100%; } .item-num-6{ width: 491px; } .item-num-7{ width: 363px; } .item-num-8{ width: 234px; } .item-num-9{ width: 106px; } }できました。
プレビューの追加に応じて、「ファイルを選択」欄のファイル数が増えていくようになりました。しかしこのままでは以下のコードによって、削除ボタンを押すとfile_fieldに格納されている全てのデータが消えてしまう状態です。
なのでコードを書き換えていきます。new_item.js//変更前 $(document).on("click", '.item-image__operetion--delete', function(){ //プレビュー要素を取得 var target_image = $(this).parent().parent() //プレビューを削除 target_image.remove(); //inputタグに入ったファイルを削除 file_field.val("") })new_item.js//変更後 //削除ボタンをクリックすると発火するイベント $(document).on("click", '.item-image__operetion--delete', function(){ //削除を押されたプレビュー要素を取得 var target_image = $(this).parent().parent() //削除を押されたプレビューimageのfile名を取得 var target_name = $(target_image).data('image') //プレビューがひとつだけの場合、file_fieldをクリア if(file_field.files.length==1){ //inputタグに入ったファイルを削除 $('input[type=file]').val(null) dataBox.clearData(); console.log(dataBox) }else{ //プレビューが複数の場合 $.each(file_field.files, function(i,input){ //削除を押された要素と一致した時、index番号に基づいてdataBoxに格納された要素を削除する if(input.name==target_name){ dataBox.items.remove(i) } }) //DataTransferオブジェクトに入ったfile一覧をfile_fieldの中に再度代入 file_field.files = dataBox.files } //プレビューを削除 target_image.remove() //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更 var num = $('.item-image').length $('#image-box__container').show() $('#image-box__container').attr('class', `item-num-${num}`) })ドラッグ&ドロップへの対応
ドロップエリアをクリックした時にファイルが入る&プレビューが表示される実装が完了したので、ドラッグ&ドロップでも画像が投稿できるようにコードを追加します。
ドラッグ時にドロップエリアに影がつくようにしてみました。
プレビュー表示、file_fieldへのfile追加に関してはクリックによるものとほとんど同じです。new_item.jsvar dropArea = document.getElementById("image-box-1"); //loadイベント発生時に発火するイベント window.onload = function(e){ //ドラッグした要素がドロップターゲットの上にある時にイベントが発火 dropArea.addEventListener("dragover", function(e){ e.preventDefault(); //ドロップエリアに影がつく $(this).children('#image-box__container').css({'border': '1px solid rgb(204, 204, 204)','box-shadow': '0px 0px 4px'}) },false); //ドラッグした要素がドロップターゲットから離れた時に発火するイベント dropArea.addEventListener("dragleave", function(e){ e.preventDefault(); //ドロップエリアの影が消える $(this).children('#image-box__container').css({'border': '1px dashed rgb(204, 204, 204)','box-shadow': '0px 0px 0px'}) },false); //ドラッグした要素をドロップした時に発火するイベント dropArea.addEventListener("drop", function(e) { e.preventDefault(); $(this).children('#image-box__container').css({'border': '1px dashed rgb(204, 204, 204)','box-shadow': '0px 0px 0px'}); var files = e.dataTransfer.files; //ドラッグアンドドロップで取得したデータについて、プレビューを表示 $.each(files, function(i,file){ //アップロードされた画像を元に新しくfilereaderオブジェクトを生成 var fileReader = new FileReader(); //dataTransferオブジェクトに値を追加 dataBox.items.add(file) file_field.files = dataBox.files //lengthで要素の数を取得 var num = $('.item-image').length + i + 1 //指定されたファイルを読み込む fileReader.readAsDataURL(file); // 10枚プレビューを出したらドロップボックスが消える if (num==10){ $('#image-box__container').css('display', 'none') } //image fileがロードされた時に発火するイベント fileReader.onload = function() { //変数srcにresultで取得したfileの内容を代入 var src = fileReader.result var html =`<div class='item-image' data-image="${file.name}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="114" height="80" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--delete'>削除</div> </div> </div>` //image-box__containerの前にhtmlオブジェクトを追加 $('#image-box__container').before(html); }; //image-box__containerにitem-num-(変数)という名前のクラスを追加する $('#image-box__container').attr('class', `item-num-${num}`) }) }) }databaseにも無事保存されましたね。めでたしめでたし。
終わりに
上記のコードを参考にして、ご自身のアプリで挙動を確かめてみてください。
無事に動くと良いですね。問題点としては
『同じ画像を貼り付けた場合に、削除を押すと複数同時に消えてしまう』
というエラーを抱えていることでしょうか・・・・・・(大問題)直したら追記します。
直せる強強エンジニアのかたいらっしゃいましたら教えてください。以上、初投稿でした。
お目通しいただきありがとうございました。












































