- 投稿日:2019-05-06T23:31:05+09:00
capistrano3でデプロイ時にslack通知する
趣味でやってるアプリ開発でcapistrano3でデプロイ実行時にslack通知を導入した際の備忘録です。
実現したいこと
自分ひとりでやってたらコンソールのログを見ていたら良いのですが、チーム開発をしてるときにデプロイ状況を確実に共有するために通知を実装しました。
deployに時間がかからないので開始時の通知はしないようにします。使ったgem
実装方法
Gemfileにslackifyを追加
Gemfilegem "capistrano-slackify"インストール
bundle install
Capfileに1行追加してrequrie
Capfilerequire "capistrano/slackify"これで使う準備が整いました。
次に、通知の設定を行います。config/deploy.rbに記述するとすべてのdeployに適用されるので、そこに書きます。
config/deploy.rb# 通知 set :slack_url, "https://hooks.slack.com/services/XXXXXXXXXXX" set :slack_channel, "#your_channel" # 開始時の通知を除いたリスト set :slack_notify_events, [:finished, :failed] # 通知内容カスタム set :slack_fields, ['status', 'stage', 'branch', 'hosts'] # デプロイ成功時 before 'slack:notify_finished', :deploy_success do set :slack_emoji, ':dancers:' set :slack_username, "Deploy成功" end # デプロイ失敗時 before 'slack:notify_failed', :deploy_failure do set :slack_emoji, ':imp:' set :slack_username, 'Deploy失敗' endslack_notify_events
通知を行うイベントのリストが入ってるので上書きします。
https://github.com/onthebeach/capistrano-slackify/blob/master/lib/capistrano/tasks/slackify.cap#L86
ここを見ると
開始、成功、失敗の3パターンにデフォルトで通知されます。
開始の通知が必要なかったため、[:finished, :failed]
と指定しました。slack_fields
通知に含めるfieldを指定します。
https://github.com/onthebeach/capistrano-slackify/blob/master/lib/capistrano/tasks/slackify.cap#L65
ここを見るとデフォルトで5つ指定されていますが、revisionは必要性を感じなかったので減らしました。
これで通知内容が1行減るので見た目的にはかなりスッキリします。最後に
非常に簡単に通知の実装をすることができるので便利でした。
失敗した時のエラー内容まで反映できたらとても素敵だなとコメントをもらったりもしました。デフォルトのイベントにdeploy:log_revisionってのがあるので、そこで使ってる情報をあれこれしたらできるかもしれないですね。参考
- 投稿日:2019-05-06T22:36:59+09:00
[メモ]継承とは
この記事は「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んだまとめです。
継承
継承はあるメッセージに応答できなければ、他のオブジェクトにそのメッセージを移譲する動きをする
スーパークラス(抽象クラス)はサブクラス間で共有される振る舞いの共有する格納場所を提供する。サブクラスがそれぞれに特化したものを用意する。いきなりスーパークラスを作るのではなく、サブクラスになり得るクラスが複数ある場合、共通の振る舞いを確認してスーパークラスに昇格する形で継承関係を作るようにする。
共通の振る舞いはスーパークラスに定義して、サブクラスで固有の振る舞いを定義する。以下の例では、サブクラスはdefault_tire_sizeを実装しているが、default_chainは実装していない。タイヤのサイズはサブクラスで定義する必要があるが、チェーンのサイズは共通しているのでスーパークラスで定義されている。(「テンプレートメソッド」パターン)
default_tire_sizeが定義されていない時にエラーを発生させることで、default_tire_sizeが定義されていないサブクラスが作られることを防いでいる。
sparesメソッドはスーパークラスで共通する値を定義して、サブクラスでそれぞれ特化した値をマージしている。オブジェクトのクラスを確認し、どのようなメッセージをそのオブジェクトに送るか決める、自分の分類を保持する変数などを確認して、自身に送るメッセージを変えるこのパターンは、相手が誰だか知っているから、特定のメッセージを送る状態であり依存関係が生まれているので注意が必要。
class Bicycle attr_reader:size,:chain,:tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size end def spares {tire_size:tire_size, chain:chain} end def default_chain '10speed' end def default_tire_size raise NotImplementedError end end class RoadBike < Bicycle attr_reader:tape_color def initialize(args) @tape_color = args[:tape_color] super(args) end def spares super.merge({tape_color:tape_color}) end def default_tire_size '23' end end class MountainBike < Bicycle attr_reader:front_shock,:rear_shock def initialize(args) @front_shock = args[:front_shock] @rear_shock = args[:rear_shock] super(args) end def spares super.merge({rear_shock:rear_shock}) end def default_tire_size '2.1' end end
- 投稿日:2019-05-06T22:31:37+09:00
[メモ] ダックタイピングとは
この記事は「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んだまとめです。
ダックタイピング
ダックタイピングはいかなる特定のクラスとも結びつかないパブリックなインターフェース
「もしもオブジェクトがダックのように鳴き、ダックのように歩くのなら、そのクラスは何であれ、それはダックである」というのが名前の由来。
パブリックインターフェースをクラスから切り離して抽象的にする。
何であるかではなく、何をするかによって定義される抽象的な型をを見つける。隠された抽象的なインターフェースを見つけることがダックタイピングにつながりクラス間の依存を取り除き柔軟な設計につながる。class Trip attr_reader:bicycles,:customers,:vehicle #この'mechanic'引数はどんなクラスのものでもよい。 def prepare(mechanic) mechanic.prepare_bicycles(bicycles) end end class Mechanic def prepare_bicycles(bicycles) bicycles.each{|bicycle|prepare_bicycle(bicycle)} end def prepare_bicycle(bicycle) #... end endTripのprepareメソッドはprepare_bicyclesに応答できるオブジェクトに依存している。旅行の準備がメカニックにメッセージを送り自転車を準備するだけであれば問題はないが、準備工程が増えた時にこのままでは以下のようになり、Tripクラスは3つのクラス名と具体的なメソッドメソッドを知ってしまい依存してしまう。
Tripはメカニックにしかメッセージを送らないと想定していると、工程が増えたときに行き詰まってしまう。class Trip attr_reader:bicycles,:customers,:vehicle def prepare(preparers) preparers.each{|preparer| case preparer when Mechanic preparer.prepare_bicycles(bicycles) when TripCoordinator preparer.buy_food(customers) when Driver preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle) end } end end class TripCoordinator def buy_food(customers) #... end end class Driver defgas_up(vehicle) #... end def fill_water_tank(vehicle) #... end end依存を取り除くための鍵となるのは、「Tripのprepareメソッドは単一の目的を果たすためにあるので、その引数も単一の目的を共に達成するために渡されてくるということを認識すること」です。
どの引数も同じ理由のためにここに存在し、その理由自体は引数の背後にあるクラスとは関係しません。MechanicもDriverもTripCoordinatorもそれぞれの役割は異なるが目的は、「旅行の準備をする者」である。この発想からダックタイプを生み出すことができる。
この視点で考えればprepare_trip(旅行の準備をする)振る舞いをするのが、「旅行の準備をする者」と言い換えることができる。抽象的な「旅行の準備をする者」というインターフェースを想定して、prepare_tripを実装しているものは「旅行の準備をする者」というダックタイプができる。MechanicもDriverもTripCoordinatorも「旅行の準備をする者」として振る舞う必要がある。
class Trip attr_reader:bicycles,:customers,:vehicle def prepare(preparers) preparers.each{|preparer| preparer.preparer_trip(self) end end class Mechanic def prepare_trip(trip) trip.bicycles.each{|bicycle| prepare_bicycle(bicycle)} end end class TripCoordinator def prepare_trip(trip) buy_food(trip.customers) end end設計をどこまで抽象的にするかはトレードオフ。ifとクラス名で分岐させて具体的にコーディングすれば必要な時間は少なく済むだろうしぱっとはわかりやすい。しかし今後の拡張、変更にコストがかかる。抽象化は手間はかかるが今後の拡張、変更は容易になる。
- 投稿日:2019-05-06T19:39:23+09:00
Ruby on railsを学習してる者たちよ、SQLを学べ!
SQLを学ぼうと思ったきっかけ
Railsで自作アプリを作っているときにN+1問題に直面した。
includesやleft_joinとかを学習してるときに、SQLの動きをちゃんとわかってないとやばくね?って思った。
Railsはあくまでフレームワークだ。本来SQL直書きするところを簡単にしてくれている。
なのでSQLをある程度わかっていたら、もし違うプログラミング言語で組むことになったとき、フレームワークであれば関連づけて覚えやすいし、直書きなら普通に書ける。
実際にSQLを勉強してみたら意外とすんなり頭に入ってくれた。何も怖がることはなかったので勉強しておくとあとあと絶対いいはず!
この記事では、結合とグループ化について書いていく。
テーブルの結合
Usersテーブル +----+--------+ | id | name | +----+--------+ | 1 | user1 | | 2 | user2 | +----+------- + Articlesテーブル +----+--------+-------+---------+ | id | title | body | user_id | +----+--------+-------+---------+ | 1 | title1 | body1 | 1 | | 2 | title2 | body2 | 1 | | 3 | title3 | body3 | 2 | | 4 | title4 | body4 | | +----+--------+-------+---------+上記の2つのテーブルがあるとします。
INNER JOIN(内部結合)
articleのuser_idとusersのidを紐づけている。
select articles.*, users.name from articles inner join users on articles.user_id = users.id;出力結果
+----+--------+-------+---------+-------+ | id | title | body | user_id | name | +----+--------+-------+---------+-------+ | 1 | title1 | body1 | 1 | user1 | | 2 | title2 | body2 | 1 | user1 | | 3 | title3 | body3 | 2 | user2 | +----+--------+-------+---------+-------+articles.user_idとusers.idが一致しているデータだけを出力している。
なので、Articlesテーブルのid = 4のデータはuser_idを持っていないので出力されていない。
LEFT JOIN(外部結合)
LEFT OUTER JOIN
はLEFT JOIN
と同じ意味。SQL
select articles.*, users.name from articles left join users on articles.user_id = users.id;出力結果
+----+--------+-------+---------+-------+ | id | title | body | user_id | name | +----+--------+-------+---------+-------+ | 1 | title1 | body1 | 1 | user1 | | 2 | title2 | body2 | 1 | user1 | | 3 | title3 | body3 | 2 | user2 | | 4 | title4 | body4 | | | +----+--------+-------+---------+-------+articlesテーブルとuserテーブルを並べるようなイメージ。
左のテーブル(articlesテーブル)を基準に結合している。
articlesテーブルはwhereの条件にあてはまる全件が出力され、それに紐づくusersのデータが出力されている。
ちなみにwhere句の条件なしの表現は
where 1 = 1
RIGHT JOIN(外部結合)
LEFT JOINの逆なので省略
グループ化
Teamsテーブル +----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamA | 3 | | 2 | teamB | 2 | | 3 | teamA | 5 | | 4 | teamB | 4 | | 5 | teamC | 10 | | 6 | teamC | 20 | +----+-------+-------+上記のテーブルがあるとします。
Group by
SQL
select team, sum(point) from teams group by team;出力結果
+----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamA | 8 | | 2 | teamB | 6 | | 3 | teamC | 30 | +----+-------+-------+teamカラムを基準にpointを合計している。
今回扱ったのはsum()合計やけど他にも色々あるで。例:
- COUNT(team)
select team, sum(point) from teams group by team;レコードのカウント数 +----+-------+-------+ | id | team | COUNT | +----+-------+-------+ | 1 | teamA | 2 | | 2 | teamB | 2 | | 3 | teamC | 2 | +----+-------+-------+
- avg(point)では平均pointが出力される。
where句の位置
SQL
select team, sum(point) from teams where team != "teamB" group by team;出力結果
+----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamA | 8 | | 2 | teamC | 30 | +----+-------+-------+Having句
group byした結果をさらに条件をつける。
SQL
select team, sum(point) from teams where team != "teamB" group by team having sum(point) > 10;出力結果
+----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamC | 30 | +----+-------+-------+何をしているかというと、
team != "teamB"の条件でグループ化
グループ化した結果をさらにsum(point) > 10の条件で検索
Havingはグループ化した後のwhere句みたいなかんじ。
余計わかりにくいかな。
おわり
他にもいろいろあるけど、とりあえず今回は結合とグループ化までで。
これがわかっただけでもRailsで走っているSQLだいたい読めると思う。
記事書いてるときたまたま見つけたサイト。
https://www.1keydata.com/jp/sql/sql-intersect.php
じゃ。
- 投稿日:2019-05-06T19:39:23+09:00
Railsを学習してる者たちよ、SQLを学べ!
SQLを学ぼうと思ったきっかけ
Railsで自作アプリを作っているときにN+1問題に直面した。
includesやleft_joinとかを学習してるときに、SQLの動きをちゃんとわかってないとやばくね?って思った。
Railsはあくまでフレームワークだ。本来SQL直書きするところを簡単にしてくれている。
なのでSQLをある程度わかっていたら、もし違うプログラミング言語で組むことになったとき、フレームワークであれば関連づけて覚えやすいし、直書きなら普通に書ける。
実際にSQLを勉強してみたら意外とすんなり頭に入ってくれた。何も怖がることはなかったので勉強しておくとあとあと絶対いいはず!
この記事では、結合とグループ化について書いていく。
テーブルの結合
Usersテーブル +----+--------+ | id | name | +----+--------+ | 1 | user1 | | 2 | user2 | +----+------- + Articlesテーブル +----+--------+-------+---------+ | id | title | body | user_id | +----+--------+-------+---------+ | 1 | title1 | body1 | 1 | | 2 | title2 | body2 | 1 | | 3 | title3 | body3 | 2 | | 4 | title4 | body4 | | +----+--------+-------+---------+上記の2つのテーブルがあるとします。
INNER JOIN(内部結合)
articleのuser_idとusersのidを紐づけている。
select articles.*, users.name from articles inner join users on articles.user_id = users.id;出力結果
+----+--------+-------+---------+-------+ | id | title | body | user_id | name | +----+--------+-------+---------+-------+ | 1 | title1 | body1 | 1 | user1 | | 2 | title2 | body2 | 1 | user1 | | 3 | title3 | body3 | 2 | user2 | +----+--------+-------+---------+-------+articles.user_idとusers.idが一致しているデータだけを出力している。
なので、Articlesテーブルのid = 4のデータはuser_idを持っていないので出力されていない。
LEFT JOIN(外部結合)
LEFT OUTER JOIN
はLEFT JOIN
と同じ意味。SQL
select articles.*, users.name from articles left join users on articles.user_id = users.id;出力結果
+----+--------+-------+---------+-------+ | id | title | body | user_id | name | +----+--------+-------+---------+-------+ | 1 | title1 | body1 | 1 | user1 | | 2 | title2 | body2 | 1 | user1 | | 3 | title3 | body3 | 2 | user2 | | 4 | title4 | body4 | | | +----+--------+-------+---------+-------+articlesテーブルとuserテーブルを並べるようなイメージ。
左のテーブル(articlesテーブル)を基準に結合している。
articlesテーブルはwhereの条件にあてはまる全件が出力され、それに紐づくusersのデータが出力されている。
ちなみにwhere句の条件なしの表現は
where 1 = 1
RIGHT JOIN(外部結合)
LEFT JOINの逆なので省略
グループ化
Teamsテーブル +----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamA | 3 | | 2 | teamB | 2 | | 3 | teamA | 5 | | 4 | teamB | 4 | | 5 | teamC | 10 | | 6 | teamC | 20 | +----+-------+-------+上記のテーブルがあるとします。
Group by
SQL
select team, sum(point) from teams group by team;出力結果
+----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamA | 8 | | 2 | teamB | 6 | | 3 | teamC | 30 | +----+-------+-------+teamカラムを基準にpointを合計している。
今回扱ったのはsum()合計やけど他にも色々あるで。例:
- COUNT(team)
select team, sum(point) from teams group by team;レコードのカウント数 +----+-------+-------+ | id | team | COUNT | +----+-------+-------+ | 1 | teamA | 2 | | 2 | teamB | 2 | | 3 | teamC | 2 | +----+-------+-------+
- avg(point)では平均pointが出力される。
where句の位置
SQL
select team, sum(point) from teams where team != "teamB" group by team;出力結果
+----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamA | 8 | | 2 | teamC | 30 | +----+-------+-------+Having句
group byした結果をさらに条件をつける。
SQL
select team, sum(point) from teams where team != "teamB" group by team having sum(point) > 10;出力結果
+----+-------+-------+ | id | team | point | +----+-------+-------+ | 1 | teamC | 30 | +----+-------+-------+何をしているかというと、
team != "teamB"の条件でグループ化
グループ化した結果をさらにsum(point) > 10の条件で検索
Havingはグループ化した後のwhere句みたいなかんじ。
余計わかりにくいかな。
おわり
他にもいろいろあるけど、とりあえず今回は結合とグループ化までで。
これがわかっただけでもRailsで走っているSQLだいたい読めると思う。
記事書いてるときたまたま見つけたサイト。
https://www.1keydata.com/jp/sql/sql-intersect.php
じゃ。
- 投稿日:2019-05-06T18:03:46+09:00
2年目の営業マンが3ヶ月プログラミングを勉強して、RubyでAmazonのランキングを表示してくれるSlack botを作ってみた
自己紹介
普段は渋谷のフィンテックベンチャーでセールスマネージャーをしており、プログラミングとは無縁の仕事をしています。ビジネスサイドが主戦場なので、物を作れる人に対する大きな憧れがあったのと、日々の業務の中でも、自分自身のシステムに対する理解の浅さから、エンジニアに迷惑をかけてしまうことがありました。また、技術的知識不足から、競合とのコンペで負けてしまい、悔しい思いをしたことがあったので、プログラミングの勉強をすることにしました。
はじめに
2月からRubyの勉強を始め、今回の10連休で簡易的なサービスを作れればと思い、UIの設計が不要なSlack botを作ることにしました。
写真のように、botにメンションを飛ばすと、botが反応し、カテゴリを選ぶと、AmazonのTOP10を表示してくれます。
詳細のコードはGithubにて共有しております。rubyのバージョンは
2.3.0
で作っています。開発の大まかな流れ
- 必要なデータをAmazonから取得する
- Slackとの連携
- Slack内でのUIの作り込み
Amazonから必要なデータをスクレイピング
今回はAmazonが提供しているAPIではなく、スクレイピングでデータを取得しています。
require 'mechanize'今回は
mechanize
というgemを使ってスクレイピングを進めていきます。# Amazon.comの対象カテゴリのランキングをスクレイピングで取得する def get_amazon_ranking(category) agent = Mechanize.new agent.user_agent_alias = "Windows Mozilla" #書かないとエラーが起こるおまじない的なもの。 page = agent.get("#{get_amazon_ranking_url(category)}")ここでは、変数categoryのデータを引っ張ってくる指示を飛ばしています。
# カテゴリー指定 def get_amazon_ranking_url(category) if category.include?('ビジネス・経済') url = 'https://www.amazon.co.jp/gp/new-releases/books/466282' else exit end return url endカテゴリー指定は上記の様な形で行い、今後、入力したいカテゴリーを増やす際は、上記にカテゴリー名とURLを入力し、if文を作るだけで追加出来ます。
データをタイトルをkeyに、URLをvalueにしてハッシュへ変換していきます。
# XMLを配列にする(Titles) def convert_array_from_xml_titles(xml_titles) titles = [] xml_titles.each_with_index do |xml_title, i| titles << "#{i + 1}:#{xml_title.inner_text.gsub(/\r\n|\r|\n|\s|\t/, "")}" break 1 if i == 9 end return titles end# XMLを配列にする(URLs) def convert_array_from_xml_urls(xml_urls) urls = [] xml_urls.each_with_index do |xml_url, i| # レビューのURLもClass名が同じなので、取得しない # 不要なクエリパラメータを削除する url = xml_url.get_attribute('href').match(/dp\/[a-zA-Z0-9]+/).to_s # レビューを除いた際に、空白が入ってしまうので削除する if url != '' urls << 'https://www.amazon.co.jp/' + url end break 1 if i == 9 end return urls endそれぞれ、
convert_array_from_xml
という形で変数を定義し、配列を用意します。Urlsの部分では、本に対するレビューのURLも同じClass名の中に入っていたため、不要なクエリパラメータを削除するための処理を行っています。
(タイトルのURLと本のレビューのURLがa-link-normal
という同一名義のClassに囲われてる)タイトル、URLともに、10個取り出せた時点でbreakする設定をしています。ここで回したデータを、TitlesとUrlsに返し、指定したクラスからデータを抜き出します。
# 必要な項目だけを抜き出す&XMLを配列にする titles = convert_array_from_xml_titles(page.search('.p13n-sc-line-clamp-2')) urls = convert_array_from_xml_urls(page.search('.a-col-left .a-link-normal')) rankings = {} titles.zip(urls) do |title, url| rankings[title] = url end return rankings endここで更にrankingsという連想配列を用意し、.zipメソッド を活用して、TitleとURLがSlackに交互に出力される指示を出します。
Slackとの連携
まず、
slack-ruby-client
というgemをインストールし、ひな形をコピペします。require 'slack-ruby-client' Slack.configure do |conf| conf.token = 'xoxb-*****************' # トークンは後ほど取得します。 end # RTM Clientのインスタンス生成 client = Slack::RealTime::Client.new # Slackに接続できたときの処理 client.on :hello do puts 'connected!' client.message channel: 'your_channel_id', text: 'connected!' end # ユーザからのメッセージを検知したときの処理 client.on :message do |data| if data['text'].include?('こんにちは') client.message channel: data['channel'], text: "Hi!" end if data['text'].include?('かしこい') || data['text'].include?('えらい') client.message channel: data['channel'], text: "Thank you!" end if data['text'].include?('おやすみ') client.message channel: data['channel'], text: "Good night" end end # Bot start client.start!次に、ブラウザ上のSlackで
Bots
から、botを作成します。
https://xxxxxxxxxxx.slack.com/apps/search?q=botsclient.message channel: 'your_channel_id', text: 'connected!'上記の部分を今回表示したい自分のSlackのチャンネルに変える作業を行います。
xoxb-*****************
の部分に、bot作成後のアクセストークンを貼ると、繋込みがされます。Slack内でのUIの作り込み
Slack内での挙動に関しては、
@レイワーくん
とスラックで呼び出すと、「僕は現在のAmazonランキング10選を教えることができるよ!★をつけてカテゴリを選んでください!」→「カテゴリー一覧」という流れで、反応するインターフェイスにしました。カテゴリー名をユーザーが入力したかどうかを検出するため、各カテゴリーの頭に★マークを入れ、★があるかどうかで、カテゴリー名と紐づけたデータを引っ張ってくる処理をif文で書いています。# ユーザからのメッセージを検知したときの処理 client.on :message do |data| if data['text'].include?('レイワーくん') || data['text'].include?('<@UJA1HUXEG>') client.message channel: data['channel'], # 追加していく! text: "僕は現在のAmazonランキング10選を教えることができるよ!\n★をつけてカテゴリを選んでください!\n```★ビジネス・経済 ★コンピュータ・IT ★科学・テクノロジー ★エンターテイメント ★歴史・地理 ★教育・学参・受験 \n★文学・評論 ★社会・政治 ★家電&カメラ ★ホーム&キッチン ★ホビー ★パソコン・周辺機器 ★ゲーム ★おもちゃ```" end # FIXME # メッセージの中に★があれば、カテゴリとしてみな if data['text'].include?('★') rankings = get_amazon_ranking(data['text']) rankings.each{|title, url| client.message channel: data['channel'], text: "#{title} \n#{url}" } end end現在、Herokuへのアップロードも進めております。詳細はまた別記事で書いていこうと思います。
作り終えての感想
簡単に作れるかと思いきや、XMLの変換や、スクレイピングでノイズを除去する作業で、思った以上に苦戦しました。一方、エラーを自力で解決出来たときの快感や、作ったものを人に見せて褒めてもらえたときの喜びは、これまでにあまり経験したことのない感動でした。未知の世界で自分の非力さを実感しながら格闘出来たことも非常に価値のある経験でした。
制作過程で死にそうな自分を何度も助けてくださった@IZUMIRU0313さんには感謝しかないです。早くエンジニアの先輩方とも肩を並べて、共闘できるくらいの戦闘力を身に着けていけるよう精進していきます。
サービスに関して、質問があれば、@matsukazu1995g1までご連絡ください!
参考記事
- 投稿日:2019-05-06T16:54:34+09:00
Railsのポリモーフィック関連の挙動確認
はじめに
公式ドキュメントのポリモーフィック関連の項目を見た際に、挙動がイメージしにくかったので、ポリモーフィック関連を持つモデルを作成しつつ、DBのデータやコンソールで動きを確認します。
環境
- OS : Ubuntu 17.04
- Ruby : 2.6.3
- Rails: 5.2.3
- MySQL: 5.7.20
ポリモーフィズム(多様性)
ポリモーフィックと聞くと、オブジェクト指向プログラミングで出てくるポリモーフィズムを思い出す人が多いはずです。
ポリモーフィズムとは、プログラミング言語の持つ性質の一つで、ある関数やメソッドなどが、引数や返り値の数やデータ型などの異なる複数の実装を持ち、呼び出し時に使い分けるようにできること。
(略)
オブジェクト指向プログラミング言語では親クラスから派生(継承)した子クラスがメソッドの内容を上書き(オーバーライド)したり、インターフェースで定義されたメソッドを実装することによりこれを実現している。オブジェクト指向プログラミングの、ポリモーフィズムでは、継承やインターフェース1を使って同名のメソッドを複数のクラスで定義します。それによって同名のメソッドでも、インスタンスごとに振る舞いを変える(多様性2が生まれる)というものです。
ただ、ActiveRecordのポリモーフィック関連は、継承やインターフェース1を用いるのではなく、ActiveRecordで定義されている関連付けの機能を使用します。
(用途は異なりますが継承を用いるシングルテーブル継承 (STI)というものもあります)
ポリモーフィック関連を持つモデルの作成
- ActiveRecordマイグレーション - Railsガイド 2.2 モデルを生成する
- ActiveRecordの関連付け(アソシエーション) - Railsガイド 2.9 ポリモーフィック関連付け
Railsガイドを元に、ポリモーフィック関連付けのモデルを作成します。
作成するモデルは、Picture/Employee/Productの3つです。
ここでは、モデルPictureと複数のモデルEmployee/Productとの関連を、imageableという関連1つで表現します。$ bin/rails generate model Product name:string Running via Spring preloader in process 21448 invoke active_record create db/migrate/20190504040556_create_products.rb create app/models/product.rb invoke test_unit create test/models/product_test.rb create test/fixtures/products.yml $ bin/rails generate model Employee name:string # 省略 $ bin/rails generate model Picture name:string # 省略PictureのMigrationファイルを修正します。
class CreatePictures < ActiveRecord::Migration[5.2] def change create_table :pictures do |t| t.string :name t.integer :imageable_id t.string :imageable_type t.timestamps end add_index :pictures, [:imageable_type, :imageable_id] end end作成した、それぞれのモデルに関連を記載します。
# app/models/picture.rb class Picture < ApplicationRecord belongs_to :imageable, polymorphic: true end # app/models/employee.rb class Employee < ApplicationRecord has_many :pictures, as: :imageable end # app/models/product.rb class Product < ApplicationRecord has_many :pictures, as: :imageable endマイグレーションを実行します。
$ bin/rails db:migrate == 20190504040556 CreateProducts: migrating =================================== -- create_table(:products) -> 0.0588s == 20190504040556 CreateProducts: migrated (0.0590s) ========================== == 20190504040742 CreateEmployees: migrating ================================== -- create_table(:employees) -> 0.0648s == 20190504040742 CreateEmployees: migrated (0.0650s) ========================= == 20190504041111 CreatePictures: migrating =================================== -- create_table(:pictures) -> 0.0630s -- add_index(:pictures, [:imageable_type, :imageable_id]) -> 0.0678s == 20190504041111 CreatePictures: migrated (0.1311s) ==========================MySQLのデータを確認
DBeaver をつかってデータを確認します。
ポリモーフィック関連を実現するために、テーブルpicturesに、imageable_id/imageable_typeというカラムが追加されてます。
imageableという単語は、モデルEmployee/Productで定義されていました。has_many :pictures, as: :imageable説明をすると、「複数のpictureを持ち、その関連はimageableとする」といったところでしょうか。
モデルの作成
動きを確認するために、モデルを作成します。
$ bin/rails c > product = Product.create(name: '商品1') => #<Product id: 1, name: "商品1", created_at: "2019-05-04 04:41:12", updated_at: "2019-05-04 04:41:12"> > employee = Employee.create(name: '田中一郎') => #<Employee id: 1, name: "田中一郎", created_at: "2019-05-04 04:42:23", updated_at: "2019-05-04 04:42:23"> > product.pictures => #<ActiveRecord::Associations::CollectionProxy []> > employee.pictures => #<ActiveRecord::Associations::CollectionProxy []>ここは
has_many
の動きです。まだPictureを作成していないのでデータがヒットしません。product.pictures.create(name: '商品1-1.jpg') => #<Picture id: 1, name: "商品1-1.jpg", imageable_id: 1, imageable_type: "Product", created_at: "2019-05-04 04:54:46", updated_at: "2019-05-04 04:54:46"> employee.pictures.create(name: '田中一郎_1.jpg') => #<Picture id: 2, name: "田中一郎_1.jpg", imageable_id: 1, imageable_type: "Employee", created_at: "2019-05-04 04:59:24", updated_at: "2019-05-04 04:59:24">それぞれのProduct/Employeeで、picturesを作成したところ、こちらで指定していないのにもかかわらずimageable_idとimageable_typeに値が入っています。
今度はPictureから、Product/Employeeの関連を見てみましょう。
> pictures = Picture.all Picture Load (0.9ms) SELECT `pictures`.* FROM `pictures` LIMIT 11 => #<ActiveRecord::Relation [#<Picture id: 1, name: "商品1-1.jpg", imageable_id: 1, imageable_type: "Product", created_at: "2019-05-04 04:54:46", updated_at: "2019-05-04 04:54:46">, #<Picture id: 2, name: "田中一郎_1.jpg", imageable_id: 1, imageable_type: "Employee", created_at: "2019-05-04 04:59:24", updated_at: "2019-05-04 04:59:24">]> > pictures.first.imageable Picture Load (0.8ms) SELECT `pictures`.* FROM `pictures` ORDER BY `pictures`.'id' ASC LIMIT 1 Product Load (1.1ms) SELECT `products`.* FROM `products` WHERE `products`.'id' = 1 LIMIT 1 => #<Product id: 1, name: "商品1", created_at: "2019-05-04 04:41:12", updated_at: "2019-05-04 04:41:12"> > pictures.last.imageable Picture Load (0.8ms) SELECT `pictures`.* FROM `pictures` ORDER BY `pictures`.'id' DESC LIMIT 1 Employee Load (1.2ms) SELECT `employees`.* FROM `employees` WHERE `employees`.'id' = 1 LIMIT 1 => #<Employee id: 1, name: "田中一郎", created_at: "2019-05-04 04:42:23", updated_at: "2019-05-04 04:42:23">(省略していた、SQL文のログも記載してます)
Pictureでは、Product/Employeeの値を参照する際に、Product/Employeeで定義していたimageableを使用して参照することができます。
ログから察するに、1回目のSQLでテーブルpicturesのデータを取得、2回目のSQLでpicturesのimageable_id/imageable_typeの値を使用して、検索するテーブルと、id値を決めているようです。
MySQLのデータを確認
コンソールで確認したように、それぞれのテーブルにデータが格納されています。
注意する点としてはpicturesのimageable_id/imageable_typeはRails(アプリ側)だと、ポリモーフィック関連を提供するカラムとして認識されますが、DB側にはポリモーフィック関連を表現する機能がないため「インデックスが張られているカラム」という認識しかありません。
例えば、アプリ側でEmployeeもしくはProductのデータを削除した際に
dependent
を使い、Pictureのimageable_id/imageable_typeのデータに手を加え関連の整合性を保つことができます。しかし、DB側でemployeeもしくはproductsを削除しても外部キー制約などを設定できない(このカラムでは外部キーとなるテーブルが明確でない)ため、picturesとの関連の整合性が取れなくなります。まとめ
- ActiveRecordのポリモーフィック関連は、ポリモーフィズムと目的は同様
- しかし、オブジェクト指向プログラミングなどと手法が異なる
- x_id/x_typeといったカラムを追加して、対象のオブジェクト(テーブル)名と主キーのidを格納する
- 1つの関連で、複数のオブジェクトの関連を表す事ができる
- ポリモーフィック関連はActiveRecordの機能であり、DBはポリモーフィックの整合性を担保しない
ここでいうインターフェースは、Javaの実装におけるインターフェースを想定しているような気がします。 ↩
オブジェクト指向プログラミングでよく例に挙げられるものは、厳密には「ポリモーフィズムの部分型付け」を指すようです。Wikipedia ポリモーフィズム ↩
- 投稿日:2019-05-06T16:33:25+09:00
Rails チュートリアル 第10章 学習内容のメモ
第10章ユーザーの更新・表示・削除
10.1 ユーザーを更新する
- ユーザー名やメールアドレスを編集するときに、毎回パスワードを入力しなくて良い仕様にする。
- パスワードがnilでも更新できるように、userモデルのバリデーションを追加する必要がある。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
- allow_nil: trueの追加したとしても、has_secure_passwordによって、新規ユーザ登録時に存在性の検証が行われる(もともとhas_secure_passwordとpresence: trueという存在性を検証する2つのバリデーションが存在していた)
10.2 認可
- 認証=サイトのユーザーを識別すること
- 認可=ユーザーが実行可能な操作を管理すること
- 第8章で認証の機能は構築したが、認可の機能は未実装
⇨どのユーザでもあらゆるアクションにアクセスできる。URLを直接編集し、別のユーザの設定変更が可能となっている- ユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する
- 具体的には、before_action(何らかの処理が実行される直前に特定のメソッドを実行する)に以下のlogged_in_userメソッドを追加する。
app/controllers/users_controller.rb# ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end end
- 投稿日:2019-05-06T16:31:23+09:00
Ruby Gold 模擬試験にたまに出てくるもの
Ruby Gold勉強3
こんにちは。
前回に続いて、Ruby Goldの学習を続けております。今は順次模擬試験を受けながら、勉強をしているので、詰まった問題・分からなかった問題について、整理していきたいと思います。
(今回の内容は、前回に比べて優先度は低めだと思われます。前回のブロックの取り扱いや、前々回のメソッド探索の方が優先度は高いので、悪しからず。)なお、今回記載させていただくのはCTC教育サービスの模擬問題を一部変更して使わせていただいております。
特異メソッド
以下のコードの説明として正しいものを選択してください。
1: obj1 = Object.new 2: def obj1.study 3: puts "Ruby Study!" 4: end 5: obj1.study 6: Object.new.study正解:6行目でエラーが発生する。
解説
Rubyでは 特定のオブジェクトにだけ メソッドを定義することができる。
これを特異メソッドと呼び、
def <オブジェクト名>.<新たに定義するメソッド名>
という風に定義する。今回の場合
obj1
というオブジェクトにだけ、特異メソッドを定義しているため、6行目で新たにオブジェクトクラスのインスタンスを生成しても、study
というメソッドは使えないため、エラーになる。
require
とload
の違い以下のようなfile1.rbとfile2.rbがあります。file2.rbを実行した結果は?
[file1.rb] $var += 1 [file2.rb] $var = 0 require "./file1.rb" ⬅︎カレントディレクトリを指定して実行 require "./file1" ⬅︎ファイルの拡張子を省略しても補完される。 puts $var正解:1
解説
require
は外部のファイルを読み込みます。
require
は次のような特徴があるため、良くライブラリの読み込みに使われます。
- 上の問題のように何度同じファイルを実行しても、一度しか実行されない。
- ファイルの拡張子などを省力しても、自動的に補完する。
一方で同じように外部のファイルを読み込むメソッドとして、
load
メソッドがあります。
こちらは、次のような特徴があルため、設定情報の読み込み用などに使われます。
- 同じファイルを実行する場合は、実行された回数だけ実行される。
- ファイルの拡張子は補完されないため、省略できない。
loadで上の問題を読み込むと次のようになりますので、動きの違いを押さえておきましょう。
[file1.rb] $var += 1 [file2.rb] $var = 0 load "./file1.rb" load "./file1.rb" puts $var => 2
undef_metdhod
とremove_method
の違い以下の2つのコードの実行結果の出力として正しいものは?(問40からそのまま抜粋させていただいております)
[コード1] class Foo def foo puts "foo" end end class Bar < Foo def foo puts "bar" end end class Bar undef_method :foo end Bar.new.foo [コード2] class Foo def foo puts "foo" end end class Bar < Foo def foo puts "bar" end end class Bar remove_method :foo end Bar.new.foo正解
コード1:エラーになる
コード2:foo(ちなみにpryでそのまま実行するとnilが返る。.rbファイルを作成して実行する)
解説
undef_method
メソッドは、クラスやモジュールのメソッドを未定義にして、呼び出せなくします。
親クラスのメソッドをサブクラスで未定義にした場合は、undef_methodを呼び出したクラスとそのサブクラスでメソッドを呼び出せなくなります。ただし、親クラスのメソッドには影響しません。ということで、[コード1]でBarクラスでundefメソッドを実行しているため、親クラスの
foo
メソッドも呼び出せません。
remove_method
はクラスやモジュールからメソッドを削除します。
undef_method
メソッドとは違い、削除できるのはそのクラス・モジュールで定義されたメソッドだけです。親クラスのメソッドは指定できません。また、削除したメソッドと同名のメソッドが親クラスにある場合は、親クラスのメソッドが呼び出されるようになります。というわけで、[コード2]では
Bar
クラスのfoo
メソッドは呼び出せなくなっていますが、親クラスのFoo
クラスのfoo
メソッドは呼び出せるため、fooが出力されます。
と、今回は模擬試験を見ながらちょっと詰まってしまった問題をまとめました。
本当は添付ライブラリの問題も分からないものがあるのですが、出題数の割には勉強する範囲が広いため、優先度を低くして、模擬試験に出たものを中心に見ていこうと思います。
- 投稿日:2019-05-06T15:05:08+09:00
【Ruby on Rails】混同しやすいredirect_toとrenderの動きと引数
はじめに
Qiita初投稿なので最初に少しだけ。
1か月ほど前にプログラミングの独学をし始め、現在はHTMLとCSSの基礎を終えて簡単なLPやサイトであれば模写してjQueryで軽く動きを付けられるくらいのレベルです。
恥ずかしながら現在ProgateでRuby on Railsを学習中です。
このレベルでQiitaに記事を書くのは大変申し訳なく思いますが、理解するのに少し時間がかかった点について、頭の中にあるふんわりとした概念を言語化して理解を定着させるためにも文章としてまとめたいと思います。自身のインプットの質を高めるためのアウトプットですが、これからプログラミングを独学される方は多いと思うので、そういった方たちに向けても書いていきます。
また、まだまだプログラミング入門者ゆえに記述内容が間違っているかもしれないので、そういった場合はご指摘いただけると幸いです。
今回躓いた点
僕が今回躓いたのは、
redirect_to
とrender
の引数がどちらともURLだと間違って理解していたためにエラーを発生してしまい少し躓きました。
redirect_to
は引数に"URL"を指定する*のに対して、renderは"フォルダ名/ファイル名"を指定します。qiita.rbredirect_to("/posts/index") #「/posts/index」というURLを指定している render("posts/index") #「postsフォルダ内にあるindexファイル」を直接指定しているちなみに、
render("/posts/index")
でも動作します。URLとして記述したのに動作していたことが今回の間違った理解につながっていました。躓いたことによって、それぞれの引数が全く別のものを指していることを理解すると同時に、この二つのメソッドの動きについてもしっかりと理解できました。
今回は各メソッドの内側の動きについて書いていきます。
redirect_toとrenderは表面上の動きは似ているが内部の動きは大きく異なる
先ほども述べましたが、
redirect_to
とrender
は表面上の動きは確かに似ていますが、表示するまでの内部の動作が大きく異なります。どういう事かというと、例えば、引数をこんな感じで取る場合を考えてみましょう。
qiita.rb# どちらとも表面上は「edit.html.erb」というビューを表示 redirect_to("/posts/index") render("posts/index")両者ともに表面上は「
edit.html.erb
」というビューを表示させます。(URL:/posts/indexのルーティングがindexアクションになっている場合。基本的にはこうなっていると思います。)しかし、表面上は「edit.html.erb」を表示するだけですが、その内部の動きは全く異なります。
Railsの表示の仕組み
内部の動きの違いについて説明する前に、Railsにおける表示の仕組みについて軽く押さえたいと思います。
Railsでは、ブラウザからHTMLファイルを要求された際に以下の順番でHTMLをブラウザに渡します。
①ルーティング
②コントローラのアクション
③ビュールーティングは、指定されたURLからどのコントローラのどのアクションで処理をするのかを決定する対応表のような役割を持っています。
そして、ルーティングによって指定されたコントローラのアクションはコントローラと同じ名前のフォルダからアクション名と同じビュー(HTMLファイル)をブラウザに返してます。redirect_toメソッドの実際の動き
表示までの大まかな流れが分かったところで話を戻します。
redirect_to("/posts/index")
とrender("posts/index")
は両者ともに表面上は「edit.html.erb」というビューを表示させますが、その内部の動きは大きく異なります。
redirect_to
は指定したURLに転送するメソッドです。URLを指定するので、先ほど説明した「①ルーティングを通って→②アクションを決定して→③ビューを表示する」という流れでビューを表示し、ブラウザからURLを指定された時と同じ動きをします。
renderメソッドの実際の動き
一方、
render
はルーティングやアクションを経由せずに直接ビューを表示することができます。ルーティングとアクションを経由せず直接ビューを表示するので、renderメソッドと同じアクション内で定義した@変数をビューで使える点が
redirect_to
との大きな違いです。まとめ
redirect_to
の引数は「"URL"」でrenderの引数は「"フォルダ名/ファイル名"」redirect_to
メソッドは引数でURLを指定するのでルーティングからアクションを通してビューを表示するrender
メソッドは引数で直接ビューを指定して表示するため、ルーティング→アクションを経由しないのでrender
メソッドと同じアクション内で定義した@変数をビューで使うことができる
- 投稿日:2019-05-06T14:51:57+09:00
rbenvをホームディレクトリ以外にインストール
概要
Linuxの場合、rbenvはGithubのREADME含めてホームディレクトリにインストールすることを前提に手順が記載されていることが多いです。
個人的にホームディレクトリ以外にインストールして利用しようと思ったのですが、その際に少し工夫が必要だったため、その方法を記載しています。環境
- Amazon Linux 2
- kernel 4.14.72-73.55.amzn2.x86_64
- bash 4.2.46
- git 2.14.5
- rbenv 1.1.2
rbenvインストール
今回は例として
/opt
配下にインストールする方向で進めていきます。# git clone https://github.com/rbenv/rbenv.git /opt/rbenv
ログイン時にPATHの読み込みと
rbenv
のセットアップを行うように設定を行います。ログインユーザ全体に反映させたい場合は
/etc/profile
に記載します。
ただし、直接同ファイルを修正するよりも、/etc/profile.d
配下に*.sh
ファイルを作成するほうが好ましいため、今回は/etc/profile.d/rbenv.sh
を新規作成して記載する方針とします。その際に、環境変数
RBENV_ROOT
に今回インストールしたrbenvのパスを定義してあげます。# cat << EOF > /etc/profile.d/rbenv.sh export RBENV_ROOT=/opt/rbenv export PATH="\$RBENV_ROOT/bin:\$PATH" eval "\$(rbenv init -)" EOFシェルを再起動して上記設定を読み込ませます。
rbenvのバージョンが表示されれば完了です。# exec $SHELL -l # rbenv -v rbenv 1.1.2-2-g4e92322次にrubyインストールのために
ruby-build
をrbenv
のプラグインとしてインストールします。# mkdir -p "$(rbenv root)"/plugins # git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-buildここまでくれば、あとはいつも通りにrubyをインストールすればOKです。
※rubyインストール前に必要なコンパイラなどもインストールしておきます。# yum -y install bzip2 gcc openssl-devel readline-devel zlib-devel # rbenv install -l Available versions: 1.8.5-p52 1.8.5-p113 1.8.5-p114 1.8.5-p115 1.8.5-p231 1.8.6 (中略) # rbenv install 2.6.3
rbenv local <ruby version>
を実行すれば各々のディレクトリでRubyのバージョンを切り替えることができます。$ pwd /home/<user> $ rbenv local 2.6.3 $ rbenv version 2.6.3 (set by /home/<user>/.ruby-version) $ ruby -v ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux] $補足
rbenv
コマンドは環境変数RBENV_ROOT
を明示的に指定していないと現在ログインしているユーザのホームディレクトリ配下をrbenv
のrootディレクトリと定義するようです。
https://github.com/rbenv/rbenv/blob/master/libexec/rbenvrbenv(抜粋)if [ -z "${RBENV_ROOT}" ]; then RBENV_ROOT="${HOME}/.rbenv" else RBENV_ROOT="${RBENV_ROOT%/}" fi export RBENV_ROOT参考
- 投稿日:2019-05-06T12:50:19+09:00
N+1の予感がしたらincludesを追加?
こんにちは!
入社したてのころ、右も左もわからずにコーディングをしていました。
そんな中で、僕もよく悩まされたN+1についての対策について簡単にまとめてみました。
N+1は簡単に防げてパフォーマンスをあげることができます。
すぐできて、効果大なのでぜひ実践してみてください!そもそもN+1とは
SQLクエリが 「データ量N + 1回 」走ってしまい、取得するデータが多くなるにつれて(Nの回数が増えるにつれて)パフォーマンスを低下させてしまう問題です。
N+1問題 / Eager Loading とは
(引用させていただきました)→簡単にいうとデータ取得の際、余計にSQLを発行してしまいパフォーマンスを下げてしまうことです。
テーブルの定義
例えば配列展開でBook(本)からAuthor(著者)の名前を出力したい場合(以下ER図&作成データになります)。
念の為コードも
Author
class Author < ApplicationRecord has_many :books endBook
class Book < ApplicationRecord belongs_to :author endデータの用意
author(諫山さん)が6冊の本(book)のリレーションを持っています。
irb(main):004:0> Author.all # 全てのAuthorレコード Author Load (0.5ms) SELECT "authors".* FROM "authors" +----+--------+-------------------------+-------------------------+ | id | name | created_at | updated_at | +----+--------+-------------------------+-------------------------+ | 1 | 諫山創 | 2019-05-05 10:44:51 UTC | 2019-05-05 10:44:51 UTC | +----+--------+-------------------------+-------------------------+irb(main):003:0> Book.all # 全てのBookレコード Book Load (1.3ms) SELECT "books".* FROM "books" +----+---------+-----------+-------------------------+-------------------------+ | id | title | author_id | created_at | updated_at | +----+---------+-----------+-------------------------+-------------------------+ | 1 | 進撃の1 | 1 | 2019-05-05 10:46:17 UTC | 2019-05-05 10:46:17 UTC | | 2 | 進撃の2 | 1 | 2019-05-05 10:46:25 UTC | 2019-05-05 10:46:25 UTC | | 3 | 進撃の3 | 1 | 2019-05-05 10:46:28 UTC | 2019-05-05 10:46:28 UTC | | 4 | 進撃の4 | 1 | 2019-05-05 10:46:31 UTC | 2019-05-05 10:46:31 UTC | | 5 | 進撃の5 | 1 | 2019-05-05 10:46:35 UTC | 2019-05-05 10:46:35 UTC | | 6 | 進撃の6 | 1 | 2019-05-05 10:46:38 UTC | 2019-05-05 10:46:38 UTC | +----+---------+-----------+-------------------------+-------------------------+irb(main):004:0> Author.first.books # 諫山さんが6冊の本(book)のリレーションを保持 Author Load (1.7ms) SELECT "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ? [["LIMIT", 1]] Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."author_id" = ? [["author_id", 1]] +----+---------+-----------+-------------------------+-------------------------+ | id | title | author_id | created_at | updated_at | +----+---------+-----------+-------------------------+-------------------------+ | 1 | 進撃の1 | 1 | 2019-05-05 10:46:17 UTC | 2019-05-05 10:46:17 UTC | | 2 | 進撃の2 | 1 | 2019-05-05 10:46:25 UTC | 2019-05-05 10:46:25 UTC | | 3 | 進撃の3 | 1 | 2019-05-05 10:46:28 UTC | 2019-05-05 10:46:28 UTC | | 4 | 進撃の4 | 1 | 2019-05-05 10:46:31 UTC | 2019-05-05 10:46:31 UTC | | 5 | 進撃の5 | 1 | 2019-05-05 10:46:35 UTC | 2019-05-05 10:46:35 UTC | | 6 | 進撃の6 | 1 | 2019-05-05 10:46:38 UTC | 2019-05-05 10:46:38 UTC | +----+---------+-----------+-------------------------+-------------------------+コンソールで実行
配列展開でBookから親モデルのAuthorのnameを呼び出すとBookのデータ数分(6個)SQLを発行してしまいます。
→つまり5回もSQLが無駄に発行されてしまうのです。books = Book.all > Book Load (0.2ms) SELECT "books".* FROM "books"irb(main):037:0* books.each do |book| irb(main):038:1* book.author.name irb(main):039:1> end Author Load (0.6ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] irb(main):040:1>この無駄なクエリを防ぐにはincludesを追加するのがもっとも簡単です
includes追加
books = Book.all.includes(:author) #追加 Book Load (2.1ms) SELECT "books".* FROM "books" Author Load (0.2ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? [["id", 1]]お気づきでしょうか、includes追加により関連レコード(author)も一緒に取得されています。
→つまり、Bookと一緒に紐づいたAuthorモデルのレコードも取得し変数に代入していることになります。Before
books = Book.all > Book Load (0.2ms) SELECT "books".* FROM "books"books = Bookモデルの全てのデータ
After
books = Bookモデルの全てのデータとそれらにひもづくAuthorデータこの状態でもう一度booksを展開してみましょう!
irb(main):051:0* books.each do |book| irb(main):052:1* book.author.name irb(main):053:1> end irb(main):054:0>今度は展開のたびにAuthorを取得していません。
SQLの発行をおさえてパフォーマンス低下を防ぐことができましたね。includesで色々なリレーションを取得する
先ほどはN:1(Book:Author)でのパターンでしたが、実際はもっと複雑な利用パターンが多いと思います。
そんな時に利用できる書き方をご紹介します。N:1 = Book:Author
books = Book.all.includes(:author)こちらは先ほどのパターンでしたね
N:1:1 = Book:Author:Profile
では、Authorのプロフィール情報を保存するAuthors::Profileがあった場合
books = Book.all.includes(author: :profile) Book Load (1.6ms) SELECT "books".* FROM "books" LIMIT ? [["LIMIT", 11]] Author Load (0.4ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? [["id", 1]] Authors::Profile Load (0.4ms) SELECT "authors_profiles".* FROM "authors_profiles" WHERE "authors_profiles"."author_id" = ? [["author_id", 1]]N:1:1:1 = Book : Author : Profile : ProfileImage
さらにProfileに1つのプロフィール写真Authors::ProfileImageひもづく場合
books = Book.all.includes(author: [profile: :profile_image]) Book Load (0.5ms) SELECT "books".* FROM "books" LIMIT ? [["LIMIT", 11]] Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? [["id", 1]] Authors::Profile Load (0.2ms) SELECT "authors_profiles".* FROM "authors_profiles" WHERE "authors_profiles"."author_id" = ? [["author_id", 1]] Authors::ProfileImage Load (0.1ms) SELECT "authors_profile_images".* FROM "authors_profile_images" WHERE "authors_profile_images"."id" = ? [["id", nil]]逆に1:N(Author:Book)なら?
これはリレーションを使い関連データを全て取得できますね。> Author.first.books Author Load (0.3ms) SELECT "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ? [["LIMIT", 1]] Book Load (0.3ms) SELECT "books".* FROM "books" WHERE "books"."author_id" = ? [["author_id", 1]]N+1の予感とタイトルにありますが、余計にクエリを投げるケースは配列展開が多いと思います。
慣れていない方はeachやmap, selectなど配列展開のメソッドを使う際にN+1が起きていないかぜひ意識してみてください!
- 投稿日:2019-05-06T10:56:19+09:00
APIサーバのレスポンスがスキーマ上のレスポンス定義と一致するかのテスト
committeeやcommittee-railsが(わからなかった|動かなかった)ので、WEB+DB PRESS Vol.108に記載されている方法でやってみた。
ざっくり言うと
oas_parser
でswagger.jsonをパースして、json_schema
でバリデーションを行うgem
gem 'oas_parser' gem 'json_schema'swagger.json
{ "swagger": "2.0", "info": { "title": "API V1", "version": "v1" }, "servers": [ { "url": "http://localhost:3000/api-docs", "description": "development server" } ], "basePath": "/api/v1", "paths": { "/areas/{id}": { "get": { "summary": "Retrieves a area", "tags": [ "Areas" ], "description": "Retrieves a specific area by id", "operationId": "getArea", "security": [ { "apiKey": [ ] } ], "produces": [ "application/json" ], "parameters": [ { "name": "id", "in": "path", "type": "integer", "description": "エリアID", "required": true } ], "responses": { "200": { "description": "success", "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ] } } } } }, "securityDefinitions": { "apiKey": { "type": "apiKey", "name": "Authorization", "in": "header" } } }SchemaオブジェクトとJSON Schemaライブラリを用いてJSONのバリデーションを行う
- spec/rails_helper.rb
RSpec.configure do |config| config.include ControllerSpecsHelper config.before :example, type: :controller do spec = OasParser::Definition.resolve(Rails.root.join('swagger', 'v1', 'swagger.json')) schema_data = spec.path_by_path(schema_path).endpoint_by_method(schema_method).response_by_code(code.to_s).raw["schema"] @schema = JsonSchema.parse!(schema_data) if schema_data end
- spec/support/controller_specs_helper.rb
module ControllerSpecsHelper def expect_to_conform_schema(response) expect { @schema.validate!(JSON.parse(response.body)) }.not_to raise_error end endテスト
require 'rails_helper' RSpec.describe Api::V1::AreasController, type: :controller do render_views describe 'GET #show' do let(:schema_path) { '/areas/{id}' } let(:schema_method) { 'get' } let(:code) { 200 } it 'conform json schema' do expect_to_conform_schema response end end end
- 投稿日:2019-05-06T07:21:52+09:00
Day004 webエンジニアへの道 - rails tutorialを始める -
こんにちは。
webエンジニアを目指すtomoです。エンジニアの勉強をしている日々の学びを記録しています。
私自身の頭の整理と今後迷った時のリファレンスも兼ねて書き連ねていきますが、同じようにエンジニア転職を考えている方の参考にもなればと考えています。
また、「ここ間違ってる!こっちが正しい!」といったご指摘もあれば頂けると嬉しいです?
[1.3.2 rails server]のブラウザ表示でつまづく
railsチュートリアルの文章に「クラウドIDEの場合は、[Share] を開いて、開きたいアプリケーションのアドレスをクリックします(図 1.11)。]と記載があり、文章に従って進めていたのですがうまく表示されませんでした。
調べた結果、文章ではなく図1.11と図1.12に従うことでブラウザを正しく表示することができました。
正しくは「クラウドIDEの場合は、[Preview]から[Preview running application]を開いて、アドレスバーの右横にあるブラウザ展開アイコンをクリックする。」のようです。
*[Share]から開く方法があれば教えてください![参考]
Cloud9上でのRuby on rails サーバ起動/ページ表示方法(Ruby on rails Tutorial 1.3.2)★メモ
*後日調べること & 気づいたこと
- チュートリアルとWikipediaでMVCモデルの概念図が異なるので、他のサイトではどのように説明されているか調べる。
さいごに
5月5,6日は勉強外の予定があり進捗少ないかもしれませんが、毎日少しでもコードを書いて進めたいと思います。
twitterもやっているので、宜しければフォローお願いします!
@tomo_tech_
- 投稿日:2019-05-06T05:38:44+09:00
【10日間でポートフォリオ作成に挑戦】9日目:フロントエンドの実装〜各種機能の修正
概要
今回は、2019年のGW期間(10日間)を全て費やして取り組む
ポートフォリオの製作過程
を取りまとめた内容を投稿させて頂きます。(投稿は毎日行う予定)全体通した取り組みの詳細については、前回までの記事をご参照ください。
【10日間でポートフォリオ作成に挑戦】1日目:要件定義〜記事投稿のCRUD
【10日間でポートフォリオ作成に挑戦】2日目:アクセス制限〜コメントのCRUD機能
【10日間でポートフォリオ作成に挑戦】3日目:ページネーション~CKEditorの導入
【10日間でポートフォリオ作成に挑戦】4日目:テーブル分割〜CKEditorのフォームへの反映
【10日間でポートフォリオ作成に挑戦】5日目:CKEditorへ画像アップロード機能を追加
【10日間でポートフォリオ作成に挑戦】6日目:テストコードの実装
【10日間でポートフォリオ作成に挑戦】7日目:検索機能〜いいね機能の実装
【10日間でポートフォリオ作成に挑戦】8日目:記事ストック機能〜ユーザーフォロー機能の実装今日一日の作業内容
ここからは、今日1日で取り組んだ作業内容をご説明します。
フロントエンドの実装
先日、サーバーサイドのの実装が概ね完了したので、全く未着手だったフロントエンドの実装を行って行きます。
と言っても、私自身、実務でフロントの実装は全く経験していないのと、残り二日という短い期間なので、手早く実装できる手段を採用します。そこで利用するのが、前回紹介したCSSフレームワークのMaterializeCSSです。
本当は、主流のBootstrapを利用したかったのですが、全く触った事が無いので、今回は諦めました。使い方としては、公式のドキュメントから、使いたいパーツやデザインを探してきて、そのデザインを適用させる為の
class
を、任意の箇所に記述するだけで、実装が完了します。
なので、全くCSSを触らなくても、WEBサイトの体裁を整える事ができます。↓公式ドキュメント
↓実装したコード
= f.submit value: t('common.button.submit'), class: 'waves-effect waves-light btn orange'そうして、
↓記事の一覧表示
↓記事の作成ページ
↓検索機能
↓ログインページ
↓マイページ
即席なので、かなり粗だらけですが、ゆくゆくはCSSフレームワークは使わずに仕上げていきたいと考えています。Vue.jsも使ってみたいですし!
今日の失敗
ここからは今日の失敗をまとめていきます
ページネーションの表示数をベタ打ち
記事の一覧を表示する箇所ではページネーションを導入していたのですが、1ページの表示件数を指定する記述は、各コードにそれぞれ記述していました。
記事の一覧は、下記のコードで表した通り、「通常の一覧表示・検索結果の一覧・ストックの一覧・自身の投稿記事の一覧」の4箇所が該当します。
controllers/posts_controller.rbdef index @posts = Post.page(params[:page]).per(10).order(id: "DESC").includes(:user) end def search @search = Post.ransack(params[:q]) @posts = @search.result.page(params[:page]).per(10) @keyword = params[:q][:title_cont] endcontrollers/users_controller.rbdef show @posts = Post.where(user_id: params[:id]).page(params[:page]).per(10).order(id: "DESC") @user = User.find(params[:id]) end def post_stocks post_stocks = current_user.post_stocks.pluck(:post_id) @posts = Post.where(id: post_stocks).page(params[:page]).per(10).order(id: "DESC") end流石に4箇所全てにベタ打ちは冗長なので、変数に置き換える事にしました。
今回は、コントローラーが別れているので、application_controller
に変数を定義しています。controllers/application_controller.rbPER = 10これで、もし表示件数を変更する必要が出て来ても、
application_controller
の記述だけ変更すれば良いので、メンテナンス性は高まります。画像アップロード機能の実装方法に無理がある
念のため、振り返りでER図を再掲します。
注目して頂きたいのが、記事に添付した画像を、どの様に管理しているか?です。
ER図では、PostDescription
と関連付けさせたimage
テーブルで管理する様にしています。そして、今回の記事の編集は
CKEditor
を利用していますが、CKEditorは、画像アップロードの操作をした時点で、画像を保存します。↓この時点
つまり、記事の新規作成の場合、記事の作成より先に、画像がDBに保存されます。
なので、画像保存時に、記事との関連付けを行う事が出来ません(まだ存在しないデータと関連付けは出来ない)CKEditorは、アップロードした画像のパスも含めて、入力した情報をHTML形式に変換して保存してくれるので、関連付けしていなくとも、編集や詳細表示は問題なく出来ます。
支障をきたすのが、記事の一覧表示で、画像を表示させる場合です。
関連付けしていないので、PostからもImageからも、互いにどのデータと紐づいているか判別出来ません。それを解消させる為に、下記の様な手段を取りました。
1:postにimage_idカラムを新たに追加し、モデルには下記のアソシエーションを記述
models/post.rbhas_one :image, dependent: :destroy2:画像保存後に、保存されたレコードのIDをセッションで保持
controllers/images_controller.rbdef create image = current_user.description_images.build( image: params[:upload], image_relation: params[:image_relation] ) if image.save render json: { url: image.image[:standard].url, uploaded: true } #画像のDBへの保存が完了したタイミングで、IDをセッションに保持 # 複数枚画像を投稿した際は、最初の画像を登録する様にnilガードで記述 session[:image_id] ||= image.id else render json: { error: { message: image.errors.full_messages }, uploaded: false } end end3:記事保存時に、セッションに保持していたImageのIDも、外部キーとして一緒に保存する
controllers/posts_controller.rbdef create @post = current_user.posts.build(post_params) @post[:image_id] = session[:image_id] if @post.save session[:image_id] = nil redirect_to post_path(@post), notice: t('common.message.post_create') else render :new end endこうする事で、あとは下記のコードで、記事一覧の中で画像も表示させる事が可能になります。
views/posts/index.html.haml.nav-wrapper.container %h5.header.orange-text = t('common.header.post_index') .row -# 要素の数だけ繰り返し = render @postsviews/posts/_post.html.haml= link_to post_path(post) do .col.s6 .post.card .card-image - if post.image_id.present? -# この記述で対象のpostと関連付いた画像を表示させる -# (:standard)は任意で変更できる様にした画像リサイズのオプション = image_tag post.image.image_url(:standard) - else = image_tag 'no_image.png' .card-title = post.title .card-user = "投稿者:#{post.user.name}"もっと良い方法がRailsやCKEdtorにはあるのかもしれませんが、あまり調査に時間を掛ける余裕も無かったため、一旦この方法で実装しました。
これについては、追い追い調査して、効果的な方法を探りたいと考えています。
明日の予定
- AWSへのデプロイ
- HTTPS化
- フロントの調整
AWSへのデプロイが1ヶ月ぶりの作業になるのと、HTTPS化が初の試みなので、本当に明日1日だけで完成できるのか?かなり不安が残ります。
いずれにしても、この10日間の開発で終わりにするのではなく、今後も継続して開発は続けて行き、機能を更にブラッシュアップさせて行こうと考えています。
※追記:10日目を投稿しました
【10日間でポートフォリオ作成に挑戦】10日目:AWSでのデプロイおまけ
最後になりますが、現在、私は下記の目標を立てて学習に取り組んでいます。
- 3年間で「10,000時間」をプログラミングに費やす
- その間、毎日ブログの投稿を行う
Twitterでは、その過程で学んだ事などを発信しています。
もし宜しければフォローしてみてください。
- 投稿日:2019-05-06T00:04:24+09:00
【Selenium】Courseraのシラバスをパース| Ruby
はじめに
- みなさんCoursera受講してますか?少しブームは過ぎましたが、GWにCourseraの機械学習を受講してみようと思ったところ、シラバスの一覧が取得できないことに気がつきました。勉強計画を立てる際に、一覧を取得したい派なのでSeleniumを用いて取得し、CSVに出力します。
- 言語はRubyです。
環境構築
- 事前にchromeのwebdriverをインストールして下さい(インストール方法は以前Qiitaで紹介されたものを挙げます: https://qiita.com/y-agatsuma/items/ea2c9845ee0a931d5c9c)
- Rubyでは、installしたgemを記したGemfileファイル(Pythonでのpip installした一覧が書いてあるファイル)があるので、本リポジトリをGithubからcloneするとすぐに使用できるようになっています
- リポジトリをcloneした後は、ターミナル上で
$ bundle install --path .bundle
と入力して下さい。その後.bundleに色々インストールされます。(Github: https://github.com/Beluuuuuuga/class_title_parser)実行方法
- ターミナル上で本ディレクトリに移動し、次のコマンドを入力します。
$ bundle exec ruby coursera_parser.rb
- その後inputでURLを入力します。(例では機械学習)
https://www.coursera.org/learn/machine-learning?
情報取得情報
パーサーについて
- Seleniumの使用方法などは色々紹介されているので割愛します。
- 授業を表すユニークなキーをデータベースに保存し、そこから全てのシラバスを取得することも考えましたが、ここでは目的の授業のURLを入力してシラバスの情報を取得します。
- sleep 1が入っていますが、これはクローラーではなくパーサーで負荷が重くなることはそれほどないと思いますが、連続で実行してしまう方がいるかもしれないので念のため入れています。
- chromeはヘッドレス(プログラム起動時に画面を出さない)を適用していますが、確認したい方はプログラムのコメントアウトを外して下さい。
require "selenium-webdriver" require 'csv' sleep 1 def chrome_setup ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36" args = ["--headless", "--no-sandbox", "--disable-gpu", "--user-agent=#{ua}", "window-size=1280x800"] #args = ["--no-sandbox", "--disable-gpu", "--user-agent=#{ua}", "window-size=1280x800"] caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {args: args}) session = Selenium::WebDriver.for :chrome, desired_capabilities: caps # session.manage.timeouts.implicit_wait = 30 session end url = gets.chop #url = "https://www.coursera.org/learn/machine-learning?" #sample_url1 機械学習 #url = "https://www.coursera.org/learn/language-processing?" #sample_url2 自然言語処理 time_ary = [] content_ary = [] session = chrome_setup wait = Selenium::WebDriver::Wait.new(:timeout => 100) session.navigate.to url title = session.find_element(:css, '.H2_1pmnvep-o_O-weightNormal_s9jwp5-o_O-fontHeadline_1uu0gyz.max-text-width-xl.m-b-1s').text session.find_element(:css, '.Button_1w8tm98-o_O-default_s8ym6d-o_O-md_1jvotax.m-t-1.d-block.m-x-auto').click #wait.until {session.find_element(:css, '').displayed?} session.find_elements(:css, '.Row_nvwp6p.SyllabusWeek.m-b-3').each do |ele| time_ary << ele.find_element(:css, '.Strong_gjs17i-o_O-weightBold_uvlhiv-o_O-fontBody_56f0wi.text-secondary').text.slice!(0) content_ary << ele.find_element(:css, '.H2_1pmnvep-o_O-weightBold_uvlhiv-o_O-bold_1byw3y2.m-b-2 ').text end csv_header = ["hour","content"] CSV.open("#{title}.csv", "w") do |csv| csv << csv_header time_ary.zip(content_ary).each do |ele| csv << ele end end session.close技術的に難しかったこと
- はじめはどこかにjsonファイルがまとまっているのでそれをまとめて取ればいいので簡単だと考えていました。しかし、上のシラバスの画像のように、全11週あるうちの第4週までが、最初の画面に存在しており、その後[詳細]ボタンをクリックすることでJavaScriptが起動してjsonファイルが読み込まれ残りの週が表示される機能になっていました。
- Seleniumに関する記事はPythonが中心だったので、参照資料が少なかったです。Pythonで使用できる方法が、Rubyで使用できないこともありました。
出力結果
- コマンド実行後、CSVファイルが作られます。CSVのタイトルは自動でtitleタグをパースするようになっています。CSVファイルはCourseraの機械学習のシラバスです。
hour,content 2,Introduction 3,Linear Regression with Multiple Variables 2,Logistic Regression 5,Neural Networks: Representation 5,Neural Networks: Learning 5,Advice for Applying Machine Learning 5,Support Vector Machines 1,Unsupervised Learning 2,Anomaly Detection 1,Large Scale Machine Learning 1,Application Example: Photo OCRおわりに
- Seleniumはハードル高いようでしたが、案外やってみると簡単で驚きました。
- Courseraでエンジニアリング力高めていきましょう!