はじめに
今回は栽培管理アプリ(デモ版公開中です)の農薬マスタに「作物名」を追加した話です。一見シンプルな変更ですが、実装してみると思わぬ問題が次々と出てきて、設計をコロコロと変えることになりました。試行錯誤の記録として残しておきます。
そもそも何が問題だったか
栽培アプリには農薬マスタがあって、農水省のCSVをインポートして使っています。アスパラガスだけ栽培していたときは気にもしなかったのですが、別の作物栽培も検討しているので、将来的なことも考えて設計しました。
例えば農薬を2種類(アスパラガスとほうれんそう)登録したとします。一覧を見るとこうなります。

「これ、どっちがアスパラ用でどっちがほうれんそう用?」となる。作物の情報がないので、同じ農薬でも作物ごとに別々に登録するしかなく、リストがどんどん膨れていきます。
しかも農薬は同じでも、作物によって希釈倍率や収穫前日数が違います。違う希釈倍率で散布したり、収穫前日数を誤ったりすると一大事です。
(例)グレーシア乳剤
├─ アスパラガスで使う場合 / 収穫前日OK
└─ ニラで使う場合 / 収穫7日前まで
解決方針:crop_nameカラムを追加する
農薬マスタはcult_pesticidesテーブルに農薬の基本情報が入っていて、希釈倍率や収穫前日数などの適用情報はcult_pesticide_applicationsテーブルに別で持っています。この適用情報テーブルにcrop_name(テキスト型)を追加することにしました。

農水省のCSVには作物名称の列があるので、インポート時にそのままcrop_nameに入れられます。設計のポイントは2つ。
- NULL = 全作物共通:手動でいれた場合なども考えてそのままNULLで残し、どの作物を選んでも表示される
- 作物名あり = 特定作物専用:「アスパラガス」なら作業登録でアスパラを栽培しているときだけ表示
SupabaseのSQL EditorでALTER TABLEを実行して、カラムを追加。既存データは一括でUPDATEしました。
ALTER TABLE cult_pesticide_applications
ADD COLUMN crop_name text;
-- 既存データを一括更新(うちはアスパラのみだったので)
UPDATE cult_pesticide_applications
SET crop_name = 'アスパラガス'
WHERE crop_name IS NULL;
初回実装後の問題:フィルターが機能しない
CSVインポート・手動登録・農薬一覧・作業登録の防除モーダルにcrop_nameを反映して、一通り動くようになりました。が、すぐに問題が出ました。

問題が3つありました。
- 農薬一覧の作物フィルターが栽培登録テーブル(
cult_crops)から作物名を取得していたため、CSVの「野菜類」「ほうれんそう」がボタンに出てこない - 栽培登録の作物名(cult_crops)と農薬CSVの作物名が一致しない。 例えばcult_cropsに「アスパラ」「アスパラガス」が混在していると 両方別ボタンで表示されてしまう。
- 「野菜類」はアスパラにもほうれんそうにも使える農薬なのに、防除モーダルで表示されない
フィルターの取得元をcult_pesticide_applicationsのdistinct crop_nameに変更して問題①は解消。防除モーダルは自動フィルタをやめて、ユーザーが手動で対象作物を選ぶドロップダウン方式に変更しました。
「自動でフィルタできたら便利」と思っていたんですが、表記ゆれや「野菜類」のような横断的な作物名への対応が難しく、誤操作防止の観点から「ユーザーが明示的に選ぶ」方式に変えました。防除記録の間違いは農薬の誤散布につながるので、ここは安全側に倒す判断です。
次の問題:「野菜類(〇〇を除く)」をどうまとめるか
農水省のCSVには「野菜類(いも類を除く)」「あぶらな科野菜(キャベツ、こまつなを除く)」のように括弧書きで細かく分類された作物名が大量にあります。これが全部別々のボタンとして表示されてしまい、収拾がつかない状態に。

最初は正規表現で括弧部分を除去してまとめようとしました。全角・半角両方に対応した正規表現を書いたんですが、それでも完全には解消できませんでした。
そこで方針を変えました。
グループ化機能の実装
「野菜類(いも類を除く)」「野菜類(いちごを除く)」「あぶらな科野菜」…これらをまとめて「ほうれんそう用」として扱いたい。でも正規表現で自動的にやろうとすると限界がある。ならば、ユーザーが自分でグループを作れる仕組みにしようと考えました。
Supabaseに2つのテーブルを追加しました。
-- グループマスタ
CREATE TABLE cult_pesticide_crop_groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid NOT NULL,
group_name text NOT NULL,
created_at timestamptz DEFAULT now()
);
-- グループに属する作物名
CREATE TABLE cult_pesticide_crop_group_members (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
group_id uuid NOT NULL REFERENCES cult_pesticide_crop_groups(id) ON DELETE CASCADE,
crop_name text NOT NULL
);
RLSは既存のmy_org_id()関数を使ってオーナー・スタッフ両方がアクセスできるように設定。my_org_id()がorganization_idを返す関数なので、同じ組織のスタッフも同じグループ設定を参照できます。
UIの動きはこんな感じです。
- 初期状態:全作物ボタンがずらっと並ぶ。「+作物をまとめる」ボタンが表示される
- ボタンを押すとモーダルが開く。グループ名を入力して、まとめたい作物名をチェックボックスで選ぶ
- 登録すると「すべて」「全作物共通」「グループ名」だけが表示されるスッキリした状態に
- 「詳細表示」ボタンを押せば元の全ボタン表示に戻せる
作業登録の防除モーダルも同様に、グループ名だけがドロップダウンに表示されるようになりました。

グループ化したら数が合わない
グループを作って動作確認すると、数字がおかしいことに気づきました。「すべて(813)」なのに「アスパラガス(353)+ほうれんそう(361)=714」で合わない。99件どこかに消えている。
原因を調べたところ、Supabaseはデフォルトで一度に取得できるデータが1000件までに制限されています。作物名一覧を取得するときにcult_pesticide_applications(1896件)から全件取得してJavaScript側でdistinctしていたんですが、1000件で打ち切られて残りが切り捨てられていました。

解決策を説明する前に、このアプリのデータ構造を少し説明します。農薬マスタ画面を開くとき、まず「農薬一覧」をデータベースから取得します。このとき各農薬に紐づく「適用詳細(作物名・希釈倍率・収穫前日数など)」も一緒に取得しています。
問題は、作物名フィルターのボタンを作るために「作物名一覧」を別途もう一度データベースにアクセスして取得していたことです。農薬一覧を取得するときにすでに作物名も含まれているのに、同じデータベースにもう一度アクセスして1000件制限に引っかかっていました。
解決策は、最初に取得した農薬一覧のデータから作物名だけを取り出して一覧を作るように変更しました。
// pesticidesデータからcropNamesを生成(別クエリ不要)
const cropNames = Array.from(
new Set(
(pesticides ?? [])
.flatMap((p) => p.cult_pesticide_applications.map((a) => a.crop_name))
.filter((name): name is string => name != null && name !== "")
)
).sort();
※なお、今回の解決策は農薬数が1000件未満であることが前提です。作物が増えて1000件を超えた場合は同じ問題が再発するため、これは今後の課題です。
作業履歴など他のテーブルでも起きる可能性がありますが、作業履歴はすでにページネーション(ページ分割)を実装済みなので、1000件を超えても問題ありません。
×ボタンがわかりにくい問題
グループボタンの横についていた×ボタン、これが「グループを削除する」ボタンなんですが、×ボタンを押して、グループだけ消えるのか、せっかく登録した農薬名まで消えるのか意味がわかりません。

と、いうことで×ボタンを▼ボタンに変更して、押すとフッターに3つの選択肢が出るようにしました。
- 編集:グループ名・メンバーの変更(今後実装)
- グループ解除:グループだけ削除、農薬データは残る
- グループと農薬を削除:グループも農薬も両方消す

作業登録側にも反映
マスタを作った主目的である、作業登録の防除モーダルに対象作物のドロップダウンを追加しました。「すべて(絞り込まない)」「アスパラガス」「ほうれんそう」のようにグループ名が表示され、選択するとその作物に対応した農薬だけに絞り込まれます。

これで「アスパラを防除するつもりがほうれんそう用の農薬を選んでしまう」という誤操作を防げます。
実装してみての感想
「カラムを1つ追加するだけ」のつもりが、農水省CSVの作物名の複雑さに引っ張られて設計を何回か変えることになりました。農業の現場では「野菜類」という登録名があり、それをシステムで扱うのが思ったより難しい。
今回の実装で特に意識したのは誤操作防止です。農薬の散布ミスは食の安全に直結するので、「便利さ」より「確実さ」を優先する判断を何度かしました。防除モーダルの自動フィルタをやめて手動選択にしたのも、その判断のひとつです。
以下のことが未実装なので追々対応していきます。
- 農薬登録1,000件以上の対応(基本、使う農薬は数十種類程度なんで、使う農薬以外削除すればいいだけではあるのですが・・一応。)
- 仮に使う農薬以外削除する場合に複数まとめて削除できるように実装
- 編集対応
- モバイル版での表示
引き続きアプリの改善記録を書いていきます。
※どうでもいい話ですが、アイキャッチ作成をChatGPTからGeminiに変更してみました。
