#はじめに
唐突ですが、40代未経験でエンジニアになりたいと言われたらどう思いますか?おそらく、「無理じゃない?」と思う方がほとんどではないかと思います。「道は厳しいよ。」この意見には同意です。自分はその道を目指しています!
今回、あるスクールに参加し、実務経験をさせていただきました。その経験から感じた趣味と実務のちがいを書いていこうと思います。構成は以下の通りです。
- プログラミング学習歴
- どんなスクールに参加したのか
- 実務体験記
- 今回学んだ趣味と実務のちがい
少しでも、挑戦したい人の参考になればと思います。
#私のプログラミング学習歴
###プログラミングに熱中したきっかけ
私はもともと小さな飲食店と温泉を経営していたのですが、「間に合わないよー」の毎日だったので『業務を効率化しよう!』と決断。そこで知り合いに紹介していただいたクラウドの「Airレジ」「freee」を導入し、昔から面倒だった会計業務を効率化しました。
その効果は絶大!毎日の会計業務に30分かかっていたのが5分になりました。
どーなってるの?が正直な感想で、そこからIT技術でできることの魅力に気づき始めましました。
###プログラミングを勉強しはじめる
2020年4月からプログラミングの勉強を始めました。学び始めるとその汎用性に驚き、どんどんのめり込んで行きました。一方で専門分野であることから基礎的スキル無くしては太刀打ちできないと感じました。
2021年10月、YouTubeで実務体験ができるエンジニアスクールのことを知り、真剣にエンジニアを目指してみようと決意し入会しました。本投稿ではその体験を書いていこうと思います。
#実務ができるエンジニアスクールについて
先生とアシスタント、私たち同期メンバー3人という構成でした。基本Slackでやりとりをしました。朝に今日のTODOを宣言、夜に振り返りを報告します。
期間は4ヶ月(実務体験2ヶ月、転職活動2ヶ月)でした。週に1回ミーティングがあり、課題の重要な部分をテーマに、考え方を説明してくれます。
基本的に、課題は自分で考えて解決しようね。わからなかったらスキルチェックシート(*)を見て解決してね。という方針でした。最初は、スキルチェックシートの重要さがわかりませんでした。基本的なことしか書いてありませんが、つまずいた時にスキルチェックシートを読み返すと、不思議と考えがまとまります。基礎的なスキルを向上する良い訓練になりました。
今回行なった実務ですが、実際に運用されている、医院紹介サイトのSEO改善を行いました。
流れとしては、クライアントへ提案書を提出し、了承を頂きます。その後実装に入り、プルリクのレビューでapprove頂いた後、最終的にGitHubのmasterへmergeされ、施策完成となります。
(*)スキルチェックシート一部抜粋 |
---|
#実務体験記
##はじめに
セクション構成と、今回の実務体験をさせていただいた案件サイトについて説明します。
###セクション構成
大まかに3部構成になっています。
- 最初の課題 SEO改善の提案(最初〜第2週)
- 次の課題 開発環境で特定ページを開く際のエラーを解決する(第2週〜第4週)
- 最後の課題 口コミ平均の星を医院ページに表示する(第5週〜実務期間終了まで)
###案件の医院紹介サイトについて
使用技術、システム構成は以下のようになります。(使用技術やシステム構成は後半でようやく理解できました。前提知識なしで課題へと進んでいきました。README、コード、本番サイトのみが手がかりでした。)
####使用技術
JavaScriptフレームワーク | Webフレームワーク | PaaS | CMS |
---|---|---|---|
Vue.js | Ruby on Rails | Heroku | Contentful |
####システム構成
#1. 最初の課題 SEO改善の提案(最初〜第2週)
ここからが実務開始となります。ローカルで環境構築、その後、対象サイトのSEO改善の提案が課題でした。
私自身の環境構築の経験は
- Eclipseでの経験
- Cloud9での経験
- VisualStadioCodeを使った経験
がありました。今まで環境構築は書いてあるようにすれば時間がかかっても構築する事はできました。今回もREADMEの通りに進めていきました。
一方で、SEOは用語は知っていても具体的に何をすれば良いのか分からない状態でした。最初から難問だと思う一方で、ワクワクしながら取り組み始めました。
##環境構築
様々な課題に直面しました。その中でも大きかったのはRails5.2から追加されたCredentialsの使用方法です。
APIにアクセスするには何らかの認証が必要になってきます。そのためのパスワードを保存しておく場所がRailsには用意されていることを知りました。
アプリケーションはGoogleマップ、Contentfulの外部リソースにアクセスしています。そのアクセスするためのパスワードを保存しておく場所でした。
####発生した問題
READMEを読みながら、Contentfulのパスワードを設定しました。しかし、ページを開こうとするとエラーが発生しました。
####問題を理解する
手当たり次第に、検索で解決法を調べる、試すを繰り返しました。自分の環境ではエラーがなくなりました。メンバーも同じところでつまずいていました。手助けしようとしたができない。自分は、なぜ解決できたのかを理解していませんでした。エラーをよく読む -> 問題を特定する -> 問題を理解したうえで解決 この流れの大事さに気づきました。特に公式のチュートリアルを読む事が大事だと気づき、その後は公式を読むようになりました。
##SEO改善の提案
SEO改善の提案を10個提出することが課題でした。環境構築は苦労しましたが、1日で達成しました。「問題を理解しなければ」と決意した矢先に、今度は大失敗してしまいました。
分析ツールという部分に興味を持ちすぎて時間がなくなり、「結果を出す」ことができませんでした。
実務の現場で、「結果を出す」ことの重要性を強く認識しました。
振り返り、すべきだったことは以下になります。
① 課題をサブタスクに分解します。この場合だったら、全体像の把握、改善提案事例の検索、改善提案に分解します。
② スケジュール管理をします。
③ それぞれのサブタスクの段階でクライアントと意思疎通をし、方向性の確認をします。
このような段階を踏むことで、効率的かつ有効な提案ができることを学びました。
#2. 次の課題 開発環境で特定ページを開く際のエラーを解決する(第2週〜第4週)
ここからは、メンバーごとにタスクが割り当てられました。ローカル環境で、Googleマップが埋め込まれているページが表示できませんでした。
前回大失敗したので、今回は課題を認識、スケジュール管理、方向性の確認をしながら進めていきました。
####原因
ローカル環境で、Google Maps APIキーをCredentialsに設定していませんでした。(クライアントよりキーの提示はできないと回答をいただく)
####当時やったこと
Clinics
コントローラーのshow
メソッドの中に原因がありました。メソッドの定義場所を確認しました。すると、app/helpers/application_helper.rb
に定義されていました。
解決法の選択肢として次のように考えました。
- Google Maps APIキーは入手することが可能なので、自分で入手して表示することができる。
- メソッドを変更してキーがない場合だけ、Googleマップを非表示にする。
手間がかからない、クライアントの環境を損ねないことを優先し、2番を提案しました。google_map_embed_api(address)
を以下のように修正しました。
- 環境変数を導入し、Google Maps APIキーがない場合には
ENV['GOOGLE_MAP_DISABLED'] = true
を記述します。 - 環境変数は文字列で取得されるので、真偽値に変換します。
- 環境変数に記述がある場合は、ブロック内をスキップさせます。
def google_map_embed_api(address)
# 以下の一行を追記
if ActiveRecord::Type::Boolean.new.cast(ENV['GOOGLE_MAP_DISABLED']).blank?
url ="*****"
key = Rails.application.credentials.***
#以下省略
ここで、先生から怒涛のダメ出し。
最終形が以下となります。
# 真偽の判定メソッドを作成
def google_map_enabled?
Rails.application.credentials.dig(***).present?
end
def google_map_embed_api(address)
return unless google_map_enabled?
url ="*****"
key = Rails.application.credentials.***
#以下省略
####学んだこと
-
ActiveRecord::Type::Boolean
に変換する必要はありませんでした。Rubyの基本的なクラスについて、理解していませんでした。真偽判定はBooleanクラスという先入観を持っていました。Rubyではfalse
とnil
は「偽」と判定、それ以外の値は全て「真」と判定します。この時、基本的なRubyの文法を一通り学習しました。Pythonとの違いを認識することで、学習コストは高くありませんでした。 -
google_map_enabled?
メソッドを作成することで、何をしているのか理解しやすくなりました。コードをわかりやすく書くこと、保守性をあげることの大事さに気づきました。 - keyが存在しない場合
credentials
メソッドではエラーになるが、dig
メソッドの場合nil
を返してくれます。そのおかげで環境変数は必要ありませんでした。 - プルリクの作成を通し、GitHubの使用方法を理解しました。その時に、概要欄の記述の仕方について、たくさんの指摘を受けました。構造化された文章の書き方について、大きな意識転換を迫られました。
#3. 最後の課題 口コミ平均の星を医院ページに表示する(第5週〜実務期間終了まで)
ここからはメインタスクとなります。前回までを振り返り、やってきたからこそ理解できる部分がある一方、今まで学んできた姿勢ではダメだと思う部分がたくさんあることに気づきました。一つ一つの理解度をより深くしていくことを意識するようにしました。
今回、私に与えられた課題は次のようになりました。
- 課題
- 口コミ平均の星を医院ページに表示する
- 条件
- サーバーの負荷をかけすぎない
この頃には、コード全体の流れが理解できるようになっていました。私たちメンバーは最後の課題かつ、これがクライアントからの報酬になるらしいので全員新たな気持ちで挑みました!
###最終的な表示
サイトを利用していただいた方には、医院相談後にアンケートを実施しています。集まったアンケートは定期的に既存のThor
タスクを行うことで、データベースに保存しています。
現状では、個別にその内容を口コミページに星マーク付きで表示しています。それぞれの医院のページには星マークが表示できていませんでした。
私たちがレストランを探す際に、よく口コミサイトを利用する機会があるかと思います。その時に一番気にするのはその店舗の⭐️マークと、平均点だと思います。SEOの観点からしてもCTR(クリック率)を改善する、重要度の高い施策です。今度こそクライアントの求める結果を出す!と意気込みました。
すべての医院ページに、医院毎の口コミ平均点に連動する星マーク、平均点、口コミ件数を表示することが求められました。
###最初にイメージしたサブタスク
- 口コミレポートのインスタンスには、属性としてトータルスコアがあり、外部キーに設定されている医院ごとの平均値を取ればよい。
- 平均点による場合分けを行い、星の表示を行う(既存サイトに平均点ではなく、個々のトータルスコアを表示するためのリソースはすでに存在していた)。
- 口コミアイコンをダウンロードして追加。
- Slimを該当ページに追加、SCSSを追加する(既存コードはSlim、Bulma、SCSSを利用していた)。
- イメージとしては食べログ、グーグルの検索表示(経営者時代は口コミをよく気にしていた)でデザインする。
- 「サーバーへの負荷をかけすぎない」要件を満たすために、データベースに必要な情報を保存。
Ruby on Railsチュートリアルをやっていたこと、簡単な管理アプリを作った経験があったこと、特にサブタスクに分解した段階でゴールまでのイメージが描けたことから、自信満々でこの課題に取り組みました。
結果が画面で表示されることとなります。楽しみではありましたが、責任も感じました。
後にプルリクでコードの何倍もあるレビューを頂いたり、施策提案書や概要欄で文章を何回も修正することになるとは全然予想していませんでした。
###施策提案書の提出
概要、課題、目的、解決策の概要、施策詳細、サブタスク、スケジュールを記入しました。実際に書いてみると、タスクの詳細部分まで全てを把握することが大事になり、理解が曖昧な言葉を多用してしまうことが多々ありました。
その度ごとに検索し、単語を納得して文章を修正していきました。また、先生やクライアントからの指摘の意味を考え、修正していきました。
この部分は特に苦労しました。少しでも曖昧な表現を用いると正確に相手に伝える事ができません。相手と意思疎通をするための文章の作成がいかに大事かを嫌というほど痛感しました。
結果、作成した施策提案書の目的、概要部分は次のようになりました。
####施策の目的
口コミ平均の星を表示することで、情報量の増加によるSEOの向上、CTRの向上。
####施策の概要
- モデルとデータベースの設計/実装
- モデルは表示内容に必要なエンティティを持ち、属性として医院毎の口コミ平均点と口コミ件数を持つようにする。
- 上記を満たすようなデータベースを作成する。主なカラムは外部キーとして
clinic_id
, 平均点を格納するaverage_score
, 口コミ件数を格納するnum_of_reports
。
- 集計・データベースへの格納タスクの実装
- 医院毎の口コミレポートにおけるトータルスコアを平均し、
average_score
に保存する。 - 医院毎の口コミレポート数を
num_of_reports
に保存する。 - タスクは
Thor
を利用して実装する。
- 医院詳細ページのビューの修正
- 医院名の下に星の表示を行う。塗り潰し、半分、塗り潰し無しの既存の画像を表示する。
- 星の表示の右に平均点を、小数第一位まで表示する。
- 口コミアイコンを表示し、口コミ件数の表示を行う。
- 件数部分からclinic_reports ( :clinic_id)へのリンクを埋めこむ。
- 医院一覧ページなどの一覧ページにおける、ビューの修正
- 3と同様に以下ページに表示。
- 医院一覧ページ
- 都道府県一覧ページ
- 市町村一覧ページ
##モデルとデータベースの実装について
ようやくコードを書く段階になりました。実際に行った実装を詳細に書いていきます。
###モデルの実装について
星マーク表示に関連するエンティティを持つ、Summaryモデルを実装しました。このモデルの持つ属性は次のようになります。
- それぞれの医院ごとに、ユーザーから頂いた口コミのトータルスコアを平均した平均点
- それぞれの医院ごとに、ユーザーから頂いた口コミの件数
ここで、大きく2つの問題に直面しました。
####単一責任の原則
1つ目は、すでにある医院のClinicモデルに属性を追加しよう、と考えてしまった点です。
医院ごとに1つの平均点を保存すればいいので、わざわざ新しいモデルを作る必要があるの?と思いました。
これは先生がおっしゃっていた、「単一責任の原則」から外れた最悪な考え方でした。クラスもメソッドも、その責任は単一で負わないとコードの保守、管理が非常に難しくなります。つまり、星マーク表示の役割のものは1つにまとめなさい。そうすれば後々誰が見てもSummaryモデルは星マークに関連したものだとすぐ理解できるでしょ、という事でした。
これは何もプログラミングに限った話でなく、
①役割を限定する事で他の部分の影響を抑える。
②役割の中で個別に改善をする事ができる。
という社会の仕組みにも通じる事ができる普遍的な原則の1つでした。
####メソッドの見えない機能
2つ目の問題点は、referencesの動作をよく理解していなかったことです。referencesとはモデルとモデルの関係性を示すものです。
例えば会社と社員の関係で言えば、
- 会社はたくさんの社員を持つ
- 社員にとっては所属する会社は1つ
のような関係性を記述します。
今回作成したSummaryモデルは、ユーザーからの口コミであるReportモデルと『1 : 多』の関係になります。
深く考えずにSummaryモデルにhas_many :reports
, Reportモデルにbelongs_to :summary
を記述していました。これが問題を引き起こします。
口コミのサンプルデータを追加しようとした際、Summary must exist
エラーが発生してしまいました。Reportモデルにbelongs_to :summary
と記述したことで、バリデーションが機能してしまいました。バリデーションが機能する?そんなことまで深く考えず、referencesを書いていました。
これが運用環境であったら、大問題を引き起こしていました。ActiveRecordの便利さと、後ろに隠れているバリデーション機能を知り、深く機能を知らずにコードを書くことの怖さを実感しました。
###テーブルの実装について
クライアントからの要件として、平均点はBigDecimal型で、小数第二位まで保存することが求められました。BigDecimal型は、誤差の扱いを厳密にしたい場合に使用します。よくある問題としてコンピュータの浮動小数点問題があります。9 ÷ 7 = 1.2857142・・・
ですが、BigDecimalではprecision(全体桁数)、scale(小数点以下の桁数)を指定する事ができます。例えば今回のように precision: 3, scale: 2
であれば1.28
のようになります。
以下のようにmigrationファイルに記述し、summariesテーブルを作成しました。
- 医院ごとの平均点を格納したいので、clinic_idを外部キーとして設定。インデックスを追加、重複なし、nullは許容しない。
- 平均点average_scoreはBigDecimal型、全体桁数3桁、小数点以下2桁、nullは許容しない、デフォルト0。
- 口コミ件数num_of_reportsは整数型、nullは許容しない、デフォルト0。
class CreateSummaries < ActiveRecord::Migration[6.1]
def change
create_table :summaries do |t|
t.references :clinic, null: false, index: { unique: true }, foreign_key: true
t.decimal :average_score, null: false, default: 0, precision: 3, scale: 2
t.integer :num_of_reports, null: false, default: 0
t.timestamps
end
end
end
###プルリク時の問題点
そもそもなのですが、今回のタスクはGitHubのmasterブランチを開発メンバーが取り込み、ローカルのブランチ上で作業し、プルリクでクライアントにapproveいただきmasterブランチへマージする。という流れになっています。
DB変更時にはmigrationファイル、structure.sql(※)ファイルの変更点がGitHubに反映されます。DB変更を伴うタスクが複数人に及んだ場合、少し工夫が必要になります。
(※) Railsのデフォルトではschema.rb
が生成されて、スキーマ管理をしてくれます。RDB固有の命令(今回はPostgreSQL)についても管理したい場合に、structure.sql
を使用することで、SQLでスキーマ管理をします。
今回、私の他にDBの変更を要するタスクを実行中のメンバーがいました。
詳細に見ていくと、migrationファイルは順番に追加され、管理されます。また、migrate
時に、timestampがstructure.sql
(※)に追加されます。そうすると後のメンバーがmasterへマージする際にコンフリクトが発生してしまいます。
そのため、今回のDB実装タスクはfeatureブランチへマージしていくつもりでしたが、切り離し、merge先をmasterブランチに変更してクライアントにマージしていただきました。複数人での開発における対処法を理解しました。
###公式で確認する
RailsガイドやGitHubのRailsのコードを見ても理解が難しく、かみくだいたサイトばっかり参考にしていました。このあたりから公式をしっかり確認し、わからない部分のみ参考サイトで検索していくようになりました。そうすると、なぜこの機能があるのか、どのような仕組みになっているのかをはっきりと理解できるようになっていきました。
最初は、Railsのデータベース作成の仕組みがわかりませんでした。Railsガイドを確認することで、migrationファイル、structure.sql(schema.rb)について、深く理解することができました。
ここもActive Recordの機能の一つでした。migrationファイルに記載されている内容からSQLを発行し、データベースを変更していました。データベーススキーマの継続的な作成機能でした。
rails db:migrate:status
で確認できますが、up
されていないmigrationファイルのみrails db:migrate
でmigrate
されます。継続的でなく、データベースを作り直す場合はrails db:reset
でデータベースをdropして、structure.sql(schema.rb)
の設計図に基づきデータベースを再構築します。migrationファイルはその時その時。schemaファイルは洗練された歴史というイメージです。
##集計・データベースへの格納タスクの実装について
データベースとモデルが作成でき、施策提案の時に一番不安だったデータの格納タスクの実装になりました。thor
はRubyで何らかのタスクを作る際に利用するライブラリです。thor
ファイル自体はRubyで書けるので違和感はありませんでした。
タスクの流れは以下のようになります。
- ユーザからの口コミはDBのreportsテーブルに保存してあり、それを取得します。
- 口コミが存在している場合のみ、口コミの項目の1つである、トータルスコアの平均値を医院ごとに算出します。
- 外部キーに医院を識別するための
clinic_id
、医院ごとの平均点、医院ごとの口コミ件数を、前回作成したデータベースに保存します。
今回のタスクを以下にまとめました。
###初めに行ったこと
lib/tasks/
に、csvファイルからDBに口コミをインポートするための、既存のhorファイルがありました。そのファイルのコードの流れを理解することから始めました。そうすると、次のような流れになっていることがわかりました。
- コンソールにタスクの開始を吐き出す
- タスクを実行する
- 結果をコンソールに吐き出す
この流れをふまえて作成することにしました。
###今回のthorファイルの持った役割
-
Clinic(has_many :reports)
に関連づけされたReport(belongs_to :clinic)
のインスタンスから、total_score
を取り出します。平均点を算出して、Summay
テーブルに医院毎の平均点、口コミ総数を保存します。 - 実行時、コンソールにはじめと終わりの合図、実行結果を吐き出します。
###実際の実務での考慮すべき点
最初に書いたコード(一部抜粋)がこちらとなります。
clinics = Clinic.all
clinics.each do |clinic|
average_score = clinic.reports.average(:total_score)
num_of_reports = clinic.reports.count
if clinic.summary.nil?
Summary.create(clinic_id: clinic.id, average_score: average_score, num_of_reports: num_of_reports)
else
summary.update_columns(average_score: average_score,num_of_reports: num_of_reports)
end
end
ここで、たくさんの指摘をいただきました!
ひとつづつ段階を追って説明します。
####N+1問題とは
clinics = Clinic.all
で一回、clinics.each do |clinic|
のブロックでn回、の合計n+1回SQLを発行してしまうことです。このように記述すると、データベースからのロードに時間がかかってしまいます。
なぜロードに時間がかかるのか?それはSQLを実行する流れを理解しないといけませんでした。以下で示しましたが3、4の前処理で時間がかかります。毎回のSQL発行時に、必ず通るのでロードに時間がかかってしまいます。
n+1問題を解決するために、eager_load
メソッドを使うことになりました。
####eager_loadメソッドとは
その部分のコードを以下に示します(一部抜粋)。Clinicのインスタンスに紐づけられた、Reportのインスタンスをeager_load
しています。Clinic.eager_load(:reports).map do |clinic|
このようにすることで、インスタンスがキャッシュされます。その後のブロックでは、SQLを発行しません。
ブロックの中では、医院の口コミがある場合のみ、summaries
に平均点、口コミ件数を保存しています。最後にcompact
で空の配列を削除し、口コミレポートのある医院だけsummaries
に格納しています。
current_time = Time.current
summaries = Clinic.eager_load(:reports).map do |clinic|
next if clinic.reports.blank?
{
clinic_id: clinic.id,
average_score: calculate_average_score(clinic),
num_of_reports: calculate_num_of_reports(clinic),
created_at: current_time,
updated_at: current_time
}
end.compact
eager_load
はClinic.eager_load(:reports)
のように書きます。この時発行するSQLは、clinicsテーブルにreportsテーブルを左外部結合して新たなテーブルを作成します。1回のSQL発行で済みますが、テーブルが大きくなると大量のキャッシュを消費します。
preload
も同じようにn+1問題の解決に使われます。指定したassociationを、複数のクエリに分けて引いてキャッシュします。今回の例であれば、2回SQLを発行します。eager_load
と違うのはテーブルを結合していないので、ロードしたテーブルで絞り込みができないことです。
今回は、それぞれのclinic
に対しreports
の平均点を求める必要があります。clinic_id
で左外部結合したテーブルを用いる必要がありました。そこで、eager_load
を使いました。
####transactionとは
データベースへデータを保存する際、途中の処理でつまずいたらデータベースを元に戻すことができます。スタート時点の状態を保存しておき、処理中のエラーをキャッチした場合にSAVEPOINTの状態を復元します。ActiveRecordの機能の一つです。
####upsert_allとは
insert_all
もそうですが、データを一括でデータベースに保存することができます。SQLの発行は1回です。upsert_all
を使うとユニークキーが重複するレコードがある場合はUpdate、重複しない場合はInsertしてくれるので一括でデータ登録したいときに便利です。
Summary.upsert_all(summaries, unique_by: :clinic_id)
のように書きました(一部抜粋)。配列とユニークキーを引数に設定します。医院のid
をユニークキーとして設定することで、summaries
テーブルにclinic_id
が存在する場合はアップデート、存在しない場合は新規作成することになります。
注意点として、モデルのバリデーションを通さないので、使用前にバリデーションを通す必要があります。今回はデータベースに保存済みのデータを用いていることから、バリデーションを省略しました。
####実行結果の表示について
どのようにすべきかかなり困惑しました。upsert_all
メソッドを用いることで複雑さが増しました。
-
clinic_id
がsummariesテーブルにある場合
平均点を、口コミに増減があった場合でも、変化がなくても再計算して保存します。 -
clinic_id
がsummariesテーブルにない場合
新規に平均点を保存します。
考えを整理すると、
-
clinicsテーブルに保存されている
clinic
-
reportsテーブルに保存されている
clinc_id
-
summariesテーブルに保存されている
clinic_id
一方で
-
reportsテーブルに保存されている
reports
-
summariesテーブルに保存されている
reportsから計算される average_score, num_of_reports
があります。それぞれが独立なので場合分けを行い、タスクの実行前後で増減を出力することが必要なのかと感じました。厳密にそれらを調べるには、複雑な過程が必要です。
この場合、**「クライアントが正常にタスクを処理できたことを確認できる」「次の行動につながる」「簡潔でわかりやすい」**を考えることが大事でした。
紆余曲折の結果、正常に保存されたSummary
の件数を表示しました。過度に複雑なことは必要なく、シンプルに表示することの大切さを認識しました。
####振り返り
このタスクについては学びが大きかったです。上記以外に、変数名の付け方、メソッド名の付け方もアドバイスをいただきました。リーダブルコード(コードを書く際のテクニック本)を読んでいますが、自分の命名能力を今後さらに改善していく必要があると感じています。
###最終的なコード
- 以下のようになリました。
class Calculate < Thor
desc 'calculate', 'calculate_summaries#average_score, num_of_reports'
# rubocop:disable Metrics/MethodLength
def calculate_summaries
puts 'Start calculating: calculate_summaries#average_score,num_of_reports'
current_time = Time.current
summaries = Clinic.eager_load(:reports).map do |clinic|
next if clinic.reports.blank?
{
clinic_id: clinic.id,
average_score: calculate_average_score(clinic),
num_of_reports: calculate_num_of_reports(clinic),
created_at: current_time,
updated_at: current_time
}
end.compact
ActiveRecord::Base.transaction do
Summary.upsert_all( # rubocop:disable Rails/SkipsModelValidations
summaries,
unique_by: :clinic_id
)
end
# rubocop:disable Layout/LineLength
puts 'Completed calculating: summaries#average_score,num_of_reports'
puts "summaries upserted: #{count_upserted_summaries(current_time)} (Clinics with reports: #{summaries.size})"
end
# rubocop:enable all
private
def calculate_average_score(clinic)
size = clinic.reports.size
size > 0 ? clinic.reports.inject(0) { |sum, report| sum + report.total_score } / size.to_f : 0
end
def calculate_num_of_reports(clinic)
clinic.reports.size
end
def count_upserted_summaries(time)
Summary.where(updated_at: time).count
end
end
##医院詳細ページのビューの修正について
モデル作成、データベース作成、格納タスク作成まで終わったので、ビューを修正していきます。このタスクでは、**『詳細ページ』**のみに絞っています。
やってみると画面の変化があるので、結構楽しかったです。以下のように作成していきました。
###口コミアイコンの追加
星のリソースは、app/assets/images/
にすでにありました。そこで、著作権フリーの口コミアイコンをネット上から探し、TinyPNGで圧縮し、他の物と同様にpng
、webp
形式で保存しました。
###表示に必要なモデルへの記述
-
平均点を小数第一位まで表示するロジック(
summaries
テーブルには小数第二位まで保存してある)を作成しました。 -
星マークの表示ロジック(塗りつぶしの星の表示回数、半分塗り潰しの星の表示回数、塗り潰しなしの星の表示回数)を以下のように書きました。
- 塗りつぶしの星(
active_star
)の表示回数
平均点を切り捨てします。返り値が、塗りつぶしをする星の数となります。 - 半分塗り潰しの星(
half_star
)の表示回数
平均点の小数点以下を四捨五入して整数にします。その値から、平均点の小数点以下を切り捨てした値を引きます。返り値は0、または1となります。 - 塗り潰しなしの星(
inactive_star
)の表示回数
5点満点なので5から上の1, 2で求めた値を引いたものが、塗り潰しなしの星の数となります。
- 塗りつぶしの星(
コードを以下に示します(一部抜粋)。
def rounded_average_score
average_score.round(1)
end
def active_star
average_score.floor
end
def half_star
average_score.round - active_star
end
def inactive_star
5 - half_star - active_star
end
###表示に必要なコントローラーへの記述
医院詳細ページのviewにおける、コントローラーのメソッドからClinicのインスタンスをpreload
している場所を発見しました。他の関連するインスタンスと一緒に、Summary
インスタンスも格納しました。
###表示に必要なviewファイルへの記述
表示させたい場所は医院詳細ページです。表示させたい部分を特定し、インスタンスを格納します。
= render 'shared/summary_detail', { clinic: @clinic, summary: @summary }
条件を満たすような、新たなパーシャルファイルを作成しました。既存の口コミを表示するためのコードが別の部分で書いてあったので、応用することで医院毎の平均点を用いた星の表示も同様な形式で書くことができました。
このファイルはSlimで書かれていること、CSSにはBulma、SCSSを用いていることに注意しました。併用されているので最初は理解が困難でした。やっていくうちに、Tileの作成や、ちょっとしたmarginの上げ下げにBulmaを使用している、DRYのためにSCSSを用いている、ことに気づきました。このことを理解すると、コードが格段に頭に入ってくるようになりました。
以下の流れで作成しました。
- 医院に口コミがある場合だけ、表示します。
- 親の
stars-section-detail
クラスを作成します(SCSSで作成)。 -
Summary
モデルのメソッドを使用して、必要な回数分、星を表示させます。 -
Summary
モデルのメソッドを使用して、平均点を表示させます。 - 口コミアイコンを表示させます。
- 口コミ件数を表示させます。口コミ件数は
Summary
モデルの属性です。件数表示は、口コミページへのリンクになっています。
コードを以下に示します(一部抜粋)。
- if summary.present?
.stars-section-detail
.stars-30
- summary.active_star.times do |n|
.star.webp
- if summary.half_star > 0
.star.webp.-half
- summary.inactive_star.times do |n|
.star.webp.-inactive
.average-score-content
= summary.rounded_average_score
.kuchikomi-icon-content
.num-of-reports-content
= link_to "#{summary.num_of_reports} 件", clinic_counseling_reports_path(clinic)
デザインを画面で確認しつつ、SCSSを追加しました。平均点のフォントは人気の口コミサイトを参考にしました。
コードを以下に示します(一部抜粋)。
.stars-section-detail {
margin-top: -10px;
margin-bottom: 10px;
.stars-30 {
display: inline-block;
.star {
& + .star {
margin-left: 3px;
}
&.-inactive {
background-image: image_url("kuchikomi_star_inactive.png.webp");
}
&.-half {
background-image: image_url("kuchikomi_star_half.png.webp");
}
&.no-webp {
background-image: image_url("kuchikomi_star.png");
&.-inactive {
background-image: image_url("kuchikomi_star_inactive.png");
}
&.-half {
background-image: image_url("kuchikomi_star_half.png");
}
}
}
}
.average-score-content {
display: inline-block;
margin-left: 10px;
color: rgb(161, 120, 7);
font-family: Arial, Helvetica, sans-serif;
font-size: 30px;
font-weight: bold;
}
.kuchikomi-icon-content {
display: inline-block;
width: 25px;
height: 25px;
margin-bottom: -5px;
margin-left: 20px;
background-image: image_url("kuchikomi_icon.png.webp");
background-size: contain;
background-position: center;
background-repeat: none;
&.no-webp {
background-image: image_url("kuchikomi_icon.png");
}
}
.num-of-reports-content {
display: inline-block;
margin-left: 10px;
font-size: 18px;
a {
border-bottom: solid 0.3px $brand-black;
color: $brand-black;
}
}
}
##医院一覧ページなどの一覧ページのビューの修正
流れを理解していたので一覧ページの表示はすんなりいきました。
###詳細ページと違うところ
- 一覧ページで使うパーシャルファイル、SCSSを同様に作成しました。
- 表示ページが医院一覧ページ、都道府県一覧ページ、市町村一覧ページと多岐にわたりました。大変だ、と思いましたが、目的とする挿入部分は全て同じパーシャルファイルから
render
されていました。そこで、そのパーシャルファイルにネストしたパーシャルファイルを作成しました。 - SCSSに関しても一覧ページ用の
stars-section-index
クラスを作成しました。
- 表示ページが医院一覧ページ、都道府県一覧ページ、市町村一覧ページと多岐にわたりました。大変だ、と思いましたが、目的とする挿入部分は全て同じパーシャルファイルから
- 一覧ページではBulmaの
tile
を使用しているので、tile
そのものがリンクになっていました。そこで、口コミ件数からのリンクは付けませんでした。 - 口コミが存在しない医院も同じページに表示されるので、
margin
を適宜追加しました。
##メインタスク完成
1ヶ月ちょっとかかり、ようやく完成しました。クライアントにmasterブランチへmergeしていただき、現在サイトへ反映されています。ほんの一部分ですが貢献できたかな、と思うとうれしさがこみ上げてきます。クライアント様には、レビューを何回もいただきました。貴重な時間を奪ってしまってしまい、迷惑になっていることが心配でしたが、「見た目も良くて気に入っています」と、最後に言っていただけました。このタスクに携わることができてよかったです。
#趣味と実務の違いは何か?
今回の実務体験を通じて、「覚悟」、「自走力」、「意識改革」が必要だと感じました。
###覚悟が必要になった
今までやってきた仕事の職域だったら、数年後の自分の姿が想像できます。家族にも迷惑をかけません。趣味でアプリを作ったり、業務効率化をすることもできます。エンジニアになれなかったときの、保険として続けるべきと考えました。
一方で、エンジニアになった自分の未来を描きます。
そんなあやふやな気持ちが、致命傷になりました。
地元で声をかけられた宿泊関連の仕事に携わっていましたが、今回の実務案件との両立がとても大変でした。コロナも落ち着き、5:30〜21:00近くまで拘束、週に2, 3回は宿直というシフトが組まれました。その中で今回のタスクをしていました。扁桃炎で寝込んだり、あくびばっかりで注意されるという事も。
このままではいけないと思い、一度だけ退会を申し出たことがありました。その時同期メンバー、スタッフ、そして先生が引き止めてくださいました。単純に嬉しかった。
結果はどうあれ、覚悟を決めた瞬間でした。今は時間の確保できる仕事をしています。
###自走力が必要になった
わからないことや指摘されたことに対して、どのように向き合っていくかです。自走力をつけることで、ずっと成長していけます。
わからないことがあると、簡単に説明してくれるサイトを好んで参照してしまいます。実はそれは断片的な情報で、根本的な理解につながらないことが多かったです。遠回りのようですが、必要だったのは公式チュートリアル、GitHubのRailsのソースコードを参照する事でした。ひとつひとつのメソッドの機能と役割を理解して、ソースコードに書いていくことを意識しました。
このことを実践していくうちに、Railsの思想とともにメソッドを深く理解することができ、問題解決の時間が短縮して行きました。
どの場面においても初めて触れることが多くありました。都度、どのように解決できるかを考え、徐々に「自走力」を養っていけたことがタスク完成につながったと感じています。
###意識改革
エンジニアは専門職であることから、基礎的なスキルを上げなければ太刀打ちできないと感じました。
実務の現場で求められる、文章を書く能力、問題の理解力、英語力などの基礎的なスキルを高めることが重要だと感じ、プログラミングの勉強と同時に基礎的スキルの向上を日々行っています。具体的には、
- 具体的にはPREP法を用いた文章作成を毎日実施。
- 英語に関しては公式ドキュメントを英語で読むことを毎日実施。
一朝一夕では解決できない問題ですが、日々継続することで、着実に改善しています。
#あとがき
とっても濃密な、2ヶ月を過ごさせていただきました。はじめてふれる技術がたくさんあり、混乱することもありました。しかし、どの技術もWeb上である程度公開され、誰でもアクセスすることができます。また、多くの先人がその技術に対し説明してくれています。誰かが問題を提起すると、違う誰かが解決法を提示してくれてます。
世界中の人々がフレームワークやモジュールを作成し、アップデートして、より開発しやすい環境を提供してくれているんだと、改めて気付きました。
アップデートの速さ、技術の進歩の速さは他の業種に比べると群を抜いていると思います。キャッチアップしてアウトプットし、少しでも将来の人々のために貢献して恩返しできるようになりたい。そう思うようになりました。
最初の一歩として、この記事がエンジニアを目指している人の力になれたなら幸いです。
まだできることは少ないですが、実務の現場での厳しさを経験できた事が、自分を大きく成長させました。さらにエンジニアになりたいという思いが強くなりました。
☆長かったと思いますが、最後までご覧いただきありがとうございます☆
☆また、一緒に頑張ったメンバー、スタッフ、クライアント、先生に感謝申し上げます☆