0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実務未経験エンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(バックエンド実装編⑥)~大学検索機能作成~

Last updated at Posted at 2025-07-24

実務未経験エンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その12)

0. 初めに

みなさんこんにちは!
このシリーズでは、プログラミング初心者の僕が頑張ってWebアプリケーションを形にするまでの記録をお届けしています。
題材は、「研究室のレビューアプリ」です!

今回は、大学を検索できる機能を作っていきたいと思います。
実は、僕自身、検索機能を実装するのは初めてなので、ワクワクしています...!!

うまく実装できるか不安ですが、分かりやすく解説していくつもりなので、今回も是非最後まで読んでみてください!

1. ブランチ運用について

前回の最後に、リモートの develop に変更をマージしたので、ローカルの develop でプルして、最新化しましょう。
develop の最新化ができたら、そこから今回は、feature/08-search-university という新しいブランチを切って作業しましょう。

2. ルーティング設定

まずは、ルーティングを設定しましょう。
検索結果を表示するためのコントローラーのメソッドが呼び出されるように新しいルーティングを追加します。

\project-root\src\routes\web.php
Route::get('/universities', [UniversityController::class, 'index'])->name('universities.index');

研究室の一覧ページを表示するための研究室コントローラーのルーティングの下あたりに追加してもらえれば OK です!
検索機能は、ログインしていないユーザーでも使えるようにしたいので、特に auth ミドルウェアのグループの中に入れる必要はありませんね。('ω')

3. コントローラー修正

大学コントローラーに検索結果を表示するメソッドを追加します。

今回は、検索結果は大学一覧ページと同じ扱いにしたいため、メソッド名は index() にして、描画する React コンポーネントも University/Index.jsx という名前にします。
というのも、大学は全国にたくさんあるため、一覧ですべて出しても自分の探したい大学を見つけることは困難だからです。
また、ホーム画面にはアプリのメイン機能であるレビューされた研究室の一覧が表示されているため、大学の方でもすべてを表示する一覧ページまではいらないかなと思います。

ということで、UniversityController.php に以下の index() メソッドを追加しましょう!

\project-root\src\app\Http\Controllers\UniversityController.php
public function index(Request $request)
    {
        $query = $request->input('query', '');

        $universities = University::query()
            ->when($query, function ($queryBuilder) use ($query) {
                $queryBuilder->where('name', 'like', '%' . $query . '%');
            })
            ->orderBy('name')
            ->paginate(10)
            ->withQueryString();

        return Inertia::render('University/Index', [
            'universities' => $universities,
            'query' => $query,
        ]);
    }

解説

では、一行ずつ解説していきたいと思います。

まず、引数の Request $request です。
今までにもたくさん出てきていますが、こちらはユーザーがブラウザの画面から送信してくるリクエストで、タイプヒントと呼ばれるものでしたね!
このように書くだけで、Laravel が自動でリクエストクラスをインスタンス化して $request という変数で使用できるようにしてくれます。
依存性の注入なんていう難しい言い方もありますが、このシリーズでずっと使ってきたものですので、そこまで身構えなくても大丈夫です。('ω')

次に、$query = $request->input('query', ''); の部分です。
input() メソッドは、リクエストインスタンスに対して使うことができる Laravel のメソッドで、ユーザーの入力内容を取得することができます!

第一引数には、ユーザーが入力するパラメータを指定します。
HTML の form タグの name 属性に対応するものです。
(本シリーズではこれまでフロントエンドの知識についてはほとんど言及してこなかったので、よくわからない方は飛ばしてもらっても大丈夫です)

第二引数には、デフォルト値を設定することができます。
空文字を設定することで、null になることを防いでいますが、ここはそれほど大事な部分ではないので割愛。(*^^)v

Laravel におけるリクエストの扱い方について、詳しく知りたい場合は、公式ドキュメントをご覧ください。

要は、ユーザーが検索するワードを取得して、$query という変数に代入しているのですよ!

続いて、以下の部分です。

\project-root\src\app\Http\Controllers\UniversityController.php
$universities = University::query()
            ->when($query, function ($queryBuilder) use ($query) {
                $queryBuilder->where('name', 'like', '%' . $query . '%');
            })
            ->orderBy('name')
            ->paginate(10)
            ->withQueryString();

University::query() とするとこで、University モデルのクエリビルダを生成することができます。

クエリビルダってなんやねん!

という人もいると思います。
今まであまりしっかりとした説明をしてきませんでしたが、そもそもなぜ SQL 文を書いていないのにDB操作ができていたのでしょうか?
データを取得して表示したりするのはもちろん、データを挿入したり、更新したり、削除したりなどのDB操作をあたかも当たり前であるかのように行ってきましたが、SQL文をそのまま書いたことはありませんでした。

なぜ SQL 文を書かないのか

データベースの基本的な操作は SQL という言語でクエリを実行することで行うことができます。
しかし、Web アプリケーションには、SQLインジェクションなどの悪意のあるユーザーからの攻撃という課題が付きまといます。

DB 操作を行うことがあるプログラムのライブラリやフレームワークの多くがこういったセキュリティ面への考慮がされているため、いわゆる素のSQL文を書いてアプリに埋め込むことは少ないようです。
もちろん、Laravel でも素のSQL文を利用することはできますが、これもやはり、そういったセキュリティ対策面を考慮した Laravel 用に少し拡張された書き方をすることが多いです。

一方で、より直観的にDB操作を行うことができる方法として、素のSQL文を書かない方法もあります。
それが、今回登場したクエリビルダORMです。

クエリビルダと ORM

これらの違いなどについては、また機会があればまとめてみたいです。
詳しく知りたい方は、このLaravel Eloquentとクエリビルダの違いを整理してみたという記事が分かりやすいかと思いますので、よければどうぞ。

ざっくりと、ORM はDBレコードをオブジェクトのメソッドを呼び出すように扱えるもので、クエリビルダはSQLを書かずにメソッドチェーンでデータベースクエリを組み立てられるツールです。

具体例を見てみましょう。
以前、UniversityController.php には store() メソッドを作成しました。

\project-root\src\app\Http\Controllers\UniversityController.php
public function store(Request $request)
    {
        $this->authorize('create', University::class);
        
        $validated = $request->validate([
            'name' => 'required|string|max:50|unique:universities,name',
        ]);

        $university = new University();
        $university->name = $validated['name'];
        $university->save();

        return redirect('/')->with('success', '大学が作成されました。'); // 一時的にトップ画面にリダイレクト
    }

これは、ORM に該当します!
save() メソッドを使うことで、このモデルに対して、保存処理が自動で行われるため、INSERT 文を書く必要がありませんでしたね。

では、これをクエリビルダで書くことはできたのでしょうか?
結論から言いますと、できます!

\project-root\src\app\Http\Controllers\UniversityController.php
public function store(Request $request)
{
    $this->authorize('create', University::class);
    
    $validated = $request->validate([
        'name' => 'required|string|max:50|unique:universities,name',
    ]);

    // クエリビルダを使った書き方
    $universityId = DB::table('universities')->insertGetId([
        'name' => $validated['name'],
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    return redirect('/')->with('success', '大学が作成されました。');
}

両者の違いを見てみましょう!

クエリビルダでは、DBファサードを用いることで、指定された文字列に対応するデータベースへの操作を行うことができます。
DB::table('universities') とすることで、Laravel が自動で Universityテーブルを紐づけます。

このように、ORM はモデルインスタンスを生成してメソッドを操作するのに対し、クエリビルダは DB を直接操作します。

ただ、クエリビルダは ORM のように save() メソッドでの保存処理を自動で行うのではなく、連想配列を作成し、保存したい項目を一つずつキーと値のペアで設定する必要があります。
また、save() メソッドでは、created_at 及び updated_at を自動で設定してくれます。

これらの点からどうやらこの store() メソッドに関しては、ORM の方がよさそうですね。

今回はどうしてクエリビルダ?

では、今回の index() メソッドはなぜクエリビルダで書くのか?
実は、そもそもクエリビルダは store() メソッドで説明したクエリビルダ以外にもあります。

store() メソッドで使われていたものは、データベースクエリビルダ と呼ばれるものです。
一方で、index() メソッドの方は、Eloquentクエリビルダと呼ばれるもので、ORM のクエリビルダ機能みたいな感じです。
これらには、確かに細かい違いがありますが、そこまで意識しなくても大丈夫かなと思います。多分...

ここまで、長々と書いてきましたが、要するに「DB操作をするものとして、素のSQL文を書くほかに ORM とクエリビルダというものがあるよ。時と場合によってそれらを使い分けることがあるよ」ということが理解できれば今のところは OK だと思います!

したがって、説明を正確にするなら、index() メソッドの University::query() の部分は確かにクエリビルダを使用していますが、他の部分は ORM も使用しています。
ORM のファサードやメソッドだけでは、今回のような検索された文字列に基づいてメソッドチェーンで条件を絞っていくというようなことはできないため、その部分だけクエリビルダを使用したという感じです。

解説の続き

脱線しましたが、中身をようやく見ていきます。

University::query() でクエリビルダを生成した後の部分からですね。
その後ろに -> をいっぱいつないでいくいわゆるメソッドチェーンをしています。

まず、->when() メソッドを見てみましょう。
これは、条件が付いたクエリを生み出すメソッドです。
第一引数に条件を、第二引数にコールバック関数を入れます。

->when($query, function ($queryBuilder) use ($query) {
                $queryBuilder->where('name', 'like', '%' . $query . '%');
            })

今回の場合だと、$query が真すなわち $query の値があれば、第二引数のコールバック関数が呼ばれ、その引数にはクエリビルダ自身が入ります。
そして、今回のコールバック関数は、少し前に出てきた無名関数です!
つまり、この無名関数の引数 $queryBuilder には、このクエリビルダ自身が入ります。

これまた少し前に出てきたコレクションじゃなくても、->when() などのメソッドは使えるようです。
その一つの例が今回のクエリビルダですね。

中身の処理ですが、->where() メソッドで、いわゆるあいまい検索をしています。
name カラムに検索したワードそのものと完全に一致するのではなく、検索ワードの前後に0文字以上の単語が含まれるものと一致するものに絞り込むというクエリにできます。
例えば、「木」と検索すると、「木」という文字が含まれるものをすべて取得し、「荒木大学」とか「荒木市立大学」とかが取得されます。

続いて、->orderBy('name')name カラム順に並べます。

->paginate(10) というのは、ページネーションと呼ばれるもので、一度に表示する件数を制限するものです。
先ほど、「全国の大学一覧がすべて出てきたら多すぎて大変」という話をしましたが、例えば検索バーに「大学」と入力されて検索されるとすべての大学が出てきてしまいます。
そんな時に、一ページには10件だけを表示して、次のページをクリックしたら次の21 ~ 30の10件が表示されるようにすると見やすいですよね。
有名どころだと、Google の検索結果とかもこれを採用していますよね。

最後の ->withQueryString(); を付けることで、クエリパラメータを次のページでも保持できるようにします。
これを付けないと、ページネーションで次のページに移動したとき、何の言葉で検索したのかを忘れてしまいます。

あとは、クエリの結果を $universities に代入して、クエリの内容と一緒に Reactコンポーネントに渡してあげます。
結構長い説明で疲れましたよね。
解説書くのすごい大変だった。(._.)

まとめると、「大学のモデルに対してデータベースをクエリビルダを用いて扱うよ。具体的には、検索ワードに一致するものに絞って、名前順に並べて、10件ごとに取得するよ」ということです。

4. 検索画面作成

コントローラーの説明で疲れたので、画面はあっさり行きましょう!(笑)
まずは、Lab/Index.jsx です。

\project-root\src\resources\js\Pages\Lab\Index.jsx
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';

export default function Index({ labs, query }) {
  const [search, setSearch] = useState(query || '');

  const handleSearch = (e) => {
    e.preventDefault();
    router.get(route('universities.index'), { query: search }); // クエリパラメータ付きでGETリクエスト
  };

  return (
    <div>
      <h1>研究室一覧</h1>
      <form onSubmit={handleSearch}>
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="大学名で検索"
        />
        <button type="submit">検索</button>
      </form>
      <div>
        {labs.map((lab) => (
          <div
            key={lab.id}
          >
            <p>{lab.name}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

5. 検索結果画面作成

次に、University/Index.jsx です。

\project-root\src\resources\js\Pages\University\Index.jsx
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';

export default function Index({ universities, query }) {
  const [search, setSearch] = useState(query || '');

  const handleSubmit = (e) => {
    e.preventDefault();
    router.get('/universities', { query: search });
  };

  return (
    <div>
      <Head title="大学検索結果" />

      <h1>大学検索</h1>

      <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="大学名で検索"
        />
        <button type="submit">検索</button>
      </form>

      {query && (
        <p><strong>{query}</strong>」の検索結果({universities.total}件)
        </p>
      )}

      {universities.data.length === 0 ? (
        <p>該当する大学は見つかりませんでした。</p>
      ) : (
        <ul>
          {universities.data.map((university) => (
            <li key={university.id}>
              {university.name}
            </li>
          ))}
        </ul>
      )}

      {/* ページネーションリンクも追加可能 */}
      {universities.last_page > 1 && (
        <div style={{ marginTop: '20px' }}>
          <p>ページ {universities.current_page} / {universities.last_page}</p>
          {/* ページネーションボタンなどを追加 */}
        </div>
      )}
    </div>
  );
}

6. 動作確認

http://localhost/labs にアクセスしてみます。

image.png

「荒木」で検索してみます。
image.png

正しく検索結果が表示されました。
URL の中にクエリパラメータが正しく埋め込まれていることも確認できます。
image.png

今度は、「テスト」と検索してみます。
image.png

出来ましたね。
image.png
このように、検索結果画面からも正しく検索できるようです。

ただ、現状、データベースには大学は10件以下しか入っていないため、ページネーションが機能しているのかが確認できません。
先ほどコントローラーで設定していた10という数字を試しに2にしてみましょう。

今度は「大学」で検索します。
image.png

4件中2件だけが表示されました。
image.png

ページ移動ボタンを押すと...
image.png

このようにページを移動してもクエリパラメータが保持され、3, 4件目のデータも無事に表示されました。

最後の、該当するものがない言葉で検索してみましょう。
image.png

image.png

問題なさそうです!

7. まとめ・次回予告

お疲れ様でした!
作業が終わったら、コミット・プッシュとプルリクエスト・マージをお忘れなく!

今回は、大学の検索機能を実装しました。
また、データベース操作の方法も勉強しましたね。

次回は、いよいよちょっと大変そうな大学・学部・研究室の編集機能を実装していきたいと思います!

参考

これまでの記事一覧

--- 要件定義・設計編 ---

--- 環境構築編 ---

--- バックエンド実装編 ---

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?