今日やったことを書き留めておきます。実務こなして復習してAtCoderも解いてQiitaにまとめてとなると相当タイムマネジメントが求められるなと感じる今日この頃。Atcoderも意地で1問だけなんとか...
- Rails復習
- React復習
- AtCoder
Rails復習
実務においてリファクタリングの最中、今までなら流したり見落としたりしてしまってた部分があったのでまとめます。
二重JOINの解消
# 2種類の任意フィルタを「必要なときだけ1回だけJOIN」して適用するサンプル
#
# - relation: ActiveRecord::Relation
# - opts: { primary_levels: [...], secondary_levels: [...] }
#
# 置換ガイド:
# - :owner と :metric は関連名(
# - Metric はJOIN先モデル
# - with_primary_levels / with_secondary_levels はJOIN先モデルのスコープ名
def apply_level_filters(relation, opts)
# どちらかのフィルタが存在する場合のみJOINを行う
if opts[:primary_levels].present? || opts[:secondary_levels].present?
# 必要最小限のJOIN(例: items -> owner -> metric)
relation = relation.joins(owner: :metric)
# 条件を段階的に追加(mergeでJOIN先モデルのスコープを適用)
if opts[:primary_levels].present?
relation = relation.merge(Metric.with_primary_levels(opts[:primary_levels]))
end
if opts[:secondary_levels].present?
relation = relation.merge(Metric.with_secondary_levels(opts[:secondary_levels]))
end
end
relation
end
これはJOINに関するコードですでにリファクタ済みとなっています。ではもともとは何が問題だったか。それは、もともとこれら二つのフィルターが同一テーブルであるmetricへ二重JOINしてしまっていたことでした。
上記のコードでは一度だけmetricテーブルにJOINし、その後は各フィルターごとにWHERE条件を追加するものとなっていて効率的な実装となっています。
メリット
- SQLの最適化: 複数の条件を一つのクエリで処理できる
- 保守性の向上: フィルター条件の追加が容易になる
JOINとpreloadの違い
こちらもリファクタを提案された際にJOINとpreloadの使い分けがわかりにくいなと思い、それぞれの役割をはっきりさせておきました。
JOINs(joins/left_joins)の役割
-
WHERE / ORDER BY / SELECT(集計)で参照するために結合
-
どのレコードを返すかをSQLレベルで決定
preloadの役割
-
取得済みレコードの関連を別クエリでまとめ取得(IN 句で一括)
-
画面表示/JSON 生成時の関連アクセスをメモリヒットにする
-
絞り込みや並べ替えには影響しない
それぞれのメリット
JOIN
-
絞り込み・並べ替え・集計が可能
-
“存在する関連だけ欲しい”ときは joins(INNER)が速い
preload
-
N+1問題を潰せる(クエリ回数がほぼ一定)
-
クエリの責務分離(フィルタはJOIN/表示はpreload)
base_queryの設計イメージ
base_query =
Caravan
.left_joins(:signals) # 例えば集計に使う関連だけ
.left_joins(guild: [:ledger]) # 並べ替えに使うならここで
.select('caravans.*', 'MAX(signals.ping_at) AS last_ping_at')
.preload(guild: [:grimoire]) # 表示用の関連は preload
-
preload は「表示用」なので常時ありでもOK
-
JOIN は「SQLで使う」ものだけを base_query に残す
N+1問題の超簡単おさらい
加えてN+1問題がわかってるようであんまりわかっていないことに気づいたのでこれについてもまとめておきます。
# 悪い例(N+1)
caravans = Caravan.limit(100) # 1回
caravans.each { |c| puts c.guild.name } # 100回(合計101回)
# 良い例(preload)
caravans = Caravan.limit(100).preload(:guild) # 2回(caravans + guilds IN (...))
caravans.each { |c| puts c.guild.name } # 追加クエリなし
つまり、どのデータを取得するをjoinsが決めており、その関連情報を取り出すためにpreloadが使われ、preloadを使うことで関連情報がメモリに保存されるのです。それ以降のデータはクエリを通して毎回DBから取得されるのではなく、メモリから取得されるので実質クエリは2回で済むという認識で大丈夫でしょう。
まとめ
- SQLで参照する?(WHERE / ORDER BY / SELECT 集計)
- はい → JOIN(必要なら joins / left_joins を選ぶ)
- いいえ(表示だけ)→ preload だけでOK
React復習
// Day3
import { useState, useRef } from 'react';
type Form = {name: string, email: string, age: string};
type Errors = Partial<Record<keyof Form, string>>;
const emailRe = /^\S+@\S+\.\S+$/;
const numRe = /^\d+$/;
export default function App() {
const [form, setForm] = useState<Form>({name: "", email: "", age: ""});
const [errors, setErrors] = useState<Errors>({});
const [touched, setTouched] = useState<Record<keyof Form, boolean>>({
name: false,
email: false,
age: false,
});
const [busy, setBusy] = useState(false);
const refs = {
name: useRef<HTMLInputElement>(null),
email: useRef<HTMLInputElement>(null),
age: useRef<HTMLInputElement>(null),
};
const validate = (f: Form): Errors => {
const e: Errors = {};
if (!f.name.trim()) e.name = "必須";
if (!emailRe.test(f.email)) e.email = "形式";
if (!numRe.test(f.age)) e.age = "数字";
return e;
};
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const k = e.target.name as keyof Form;
const next = {... form, [k]: e.target.value};
setForm(next);
setErrors(validate(next));
};
//触れた項目だけエラー見せる
const onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const k = e.target.name as keyof Form;
//「直前の state の値」が t に入り、前の値をベースにして部分的に更新する
setTouched((t) => ({... t, [k]: true}));
};
//エラー項目のうち最初の一つを取得するための関数
const firstErrorKey = (e: Errors): keyof Form | undefined =>
(['name', 'email', 'age'] as (keyof Form)[]).find((k) => e[k]);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setTouched({name: true, email: true, age: true});
const v = validate(form);
setErrors(v);
//エラー項目の最初の一つを取得し、エラーがあればフォーカス
const bad = firstErrorKey(v);
if (bad) {
refs[bad].current?.focus();
return;
}
setBusy(true);
try {
await new Promise((r) => setTimeout(r, 300));
console.log("payload", {... form, age: Number(form.age)});
setForm({name: "", email: "", age: ""});
setErrors({});
setTouched({name: false, email: false, age: false});
} finally {
setBusy(false);
}
};
const show = (k: keyof Form) => touched[k] && errors[k];
const hasError = Object.keys(errors).length > 0;
return (
<form onSubmit={onSubmit}>
<h1>フォーム</h1>
<div>
<input
ref={refs.name}
name="name"
placeholder="Name"
value={form.name}
onChange={onChange}
onBlur={onBlur}
/>
{show("name") && <span style={{ color: "red"}}>{errors.name}</span>}
</div>
<div>
<input
ref={refs.email}
name="email"
placeholder="Email"
value={form.email}
onChange={onChange}
onBlur={onBlur}
inputMode="email"
/>
{show("email") && <span style={{ color: "red"}}>{errors.email}</span>}
</div>
<div>
<input
ref={refs.age}
name="age"
placeholder="Age"
value={form.age}
onChange={onChange}
onBlur={onBlur}
inputMode="numeric"
/>
{show("age") && <span style={{ color: "red"}}>{errors.age}</span>}
</div>
<button type ="submit" disabled={busy || hasError}>
{busy ? "送信中..." : "送信"}
</button>
</form>
)
}
流れとしては
①型定義
②バリデーションの正規定義
③App内での各定義
④自動フォーカス用のref定義
⑤バリデーション定義
⑥値更新+即時バリデーション
⑦触れた項目のみエラー
⑧最初のエラー定義
⑨送信定義
⑩エラー文表示条件
以下は初見で理解できなかった部分についてまとめています。
const firstErrorKey = (e: Errors): keyof Form | undefined =>
(["name", "email", "age"] as (keyof Form)[]).find((k) => e[k]);
役割
- エラーが付いている最初のフィールド名("name" | "email" | "age" のどれか)を返す
- もしどのフィールドにもエラーがないなら undefined を返す
try {
// ここにメインの処理
} catch (err) {
// エラーが起きたときの処理
} finally {
// 成功しても失敗しても最後に必ず実行される処理
}
- try:普通にやりたい処理
- catch:エラーがあったらここで受け止める
- finally:結果に関わらず最後に必ず実行(後片付け)
current?の「?」とは
実務でもいろんなところで見かけていましたが、わかってるようでちゃんと理解していなかったこのオプショナルチェイニング演算子。
このrefs[bad].current?.focus() という書き方は、「refs[bad].current が もし存在していたら .focus() を呼び出す」という意味になります。
refs[bad].current.focus(); // ❌ current が null の場合エラー
refs[bad].current?.focus(); // ✅ null の場合は何もしない
AtCoder
さて、今日はA問題を1問だけ解きました。ちなみに馬鹿正直にA問題を全部やろうとしてるのではなく、比較的正答率が低くて難易度が高めのものを選んでます。で、10分以内に解けないとやり直しという縛りに緩めましたがまだまだ実力が足らないようです...
ABC 345 A 問題
最初に解いた時のコードが以下になります。
S = input()
for i in range(len(S)):
if (i == 0) and S[i] != "<":
print("No")
exit()
elif (i != len(S) - 1) and S[i] != "=":
print("No")
exit()
else:
if (S[i] != ">"):
print("No")
exit()
print("Yes")
いや、もうだいぶ終わってますよね。if文の使い方がまるでなってないです。これくらいはできるだろうと意気込んだのですが、まだまだ10分以内という制約の中で解けないクソ雑魚プログラマーであることが露呈してしまいました。
問題点
その1:
i == 0 のとき最初の if が通っても、そのあと elif の条件も評価されてしまい、
「最後の文字じゃないのに = じゃない」という判定に引っかかる。
→ 先頭 < ですらアウトになるパターン。
その2:
elif の条件に該当しなかった場合=「最後の文字のとき」しか else に行かないので、
間の文字(真ん中)が正しく = チェックできない。
だから、位置(先頭・末尾・中間)ごとに条件を完全に分ける方が安全だったんですね〜。この反省を踏まえて実装したプログラムが以下の通り。
# 回答時間 2:50
# 実力がないうちはかっこつけてスタイリッシュなコードは書こうとしないほうがいい
S = input()
for i in range(len(S)):
if (i == 0):
if (S[i] != "<"):
print("No")
exit()
elif (i != len(S) - 1):
if (S[i] != "="):
print("No")
exit()
else:
if(S[i] != ">"):
print("No")
exit()
print("Yes")
ほらほら3分以内に解けた!これくらいはこの時間で解けるポテンシャルはあるはずなんですよ。「こんな簡単な問題ごときにはしゃぐんじゃねえ」って声が聞こえてきそうですが、3ヶ月後にはC問題がスラスラ解けてるので自分のペースでやっていきましょう。
コメントにもある通り、実力が伴わないうちからかっこつけた実装はせず、汚くても愚直に動くコードを書けるようにしましょうねってことですな。