個人サイト制作:サイトマップへ戻る

JSONとJavaScriptを使って簡易的なNoSQL風データベースを作る

まず、ここでご紹介するコードは「静的JSONファイルを使ったクライアントサイド表示」であり、正しくはNoSQLとは呼びません。
このサイトは静的な部分と動的な部分が分かれていて、目的に応じて実装する場所を使い分けられるようになっています。
今回の簡易データベースくらいなら、静的なところだけで済ませられるというだけで。
NoSQLを使うには、データベース(MongoDBなど)+APIサーバー(Node.jsなど)が必要となります。
ただし、この構成は軽量で高速であり、小規模なデータ表示には最適なんですよね。

例えばこのサイトで使っているPythonのFlaskであれば、次のようなパーツに分けて考えます。

MongoDB
データを保存する場所。JSON形式で保存できる。
例:ユーザー情報、投稿、コメントなど。
pymongo
FlaskからMongoDBにアクセスするためのPythonライブラリ。
例:users_collection.find() など。
Flask
APIサーバー。クライアントからのリクエストを受けて、MongoDBからデータを取得して返す。
例:/api/users などのエンドポイント。
HTML + JS
ユーザーが操作する画面。JavaScriptでAPIを呼び出して、データを表示する。
例:fetch('/api/users') など。

このような構成でNoSQLを作っていくと、次のようなメリットがあります。

そして、例えば「ユーザー一覧」を表示する流れを書くと、次のようになります。

  1. ユーザーがブラウザでページを開く
  2. JavaScriptが /api/users にリクエストを送る
  3. Flaskがリクエストを受けて、MongoDBからユーザー一覧を取得
  4. FlaskがJSON形式でデータを返す
  5. JavaScriptがデータを受け取って、HTMLに描画する

つまり、これからご紹介する「NoSQLらしく見えるデータベース」は、上記の1.4.5だけを抜粋したものと言えます。
静的なJSONファイルを読み込んで表示しており、クライアントサイドのみで完結させています。
API通信やDB連携は含まれていませんが、個人サイトでデータ管理をする程度であれば、このレベルの実装で十分でしょう。

feinのミニデータベース

手っ取り早く作るなら、こんな感じでしょうかね。
下記のデータベースは単純なJSONファイルから読み込んでいるので、中身を更新したいなら、そのJSONファイルを編集するだけです。

読み込み中...

実際に触ってみていただいて構いません。
後ろでいろいろ動かしているわけではないので、軽快に動作すると思いますよ?

HTMLの記載例

そんなに複雑ではありません。
データを表示させるための枠組みを整えているだけです。


<input type="text" id="search-name" placeholder="名前で検索">
<select id="role-filter">
    <option value="">すべての役割</option>
    <option value="admin">Admin</option>
    <option value="editor">Editor</option>
    <option value="user">User</option>
</select>

<select id="sort-order">
    <option value="id-asc">ID昇順</option>
    <option value="id-desc">ID降順</option>
    <option value="name-asc">名前昇順</option>
    <option value="name-desc">名前降順</option>
</select>

<div id="fein-user-list">読み込み中...</div>
<div id="role-count"></div>

<script src="/contents/nosql/feinnosql.js"></script>

HTMLコードを1行ずつ丁寧に解説します。
技術的な背景やUI設計の意図も交えて、読みやすく構造的に整理しました。

入力・選択フォーム部分

<input type="text" id="search-name" placeholder="名前で検索">

テキスト入力欄です。
ユーザーが名前で検索するためのキーワードを入力します。


<select id="role-filter">

役割によるフィルタリングを行うセレクトボックスの開始タグです。


<option value="">すべての役割</option>

初期選択肢です。
「すべての役割」を選ぶと、フィルタがかからず全ユーザーが表示されます。


<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="user">User</option>

それぞれの役割に対応する選択肢です。


</select>

セレクトボックスの終了タグです。


<select id="sort-order">

並び順を指定するセレクトボックスの開始タグです。


<option value="id-asc">ID昇順</option>
<option value="id-desc">ID降順</option>

ユーザーIDによる昇順・降順の並び替えを指定する選択肢です。


<option value="name-asc">名前昇順</option>
<option value="name-desc">名前降順</option>

ユーザー名による昇順・降順の並び替えを指定する選択肢です。


</select>

並び順セレクトボックスの終了タグです。

表示領域

<div id="fein-user-list">読み込み中...</div>

ユーザー一覧を表示する領域です。


<div id="role-count"></div>

役割ごとの人数集計結果を表示する領域です。

スクリプトの読み込み

<script src="/contents/nosql/feinnosql.js"></script>

JavaScriptファイル feinnosql.js を読み込むタグです。

JSONの記載例

お次はJSONですが、これはワンパターンに書いていくだけですよ。


[
 {
  "id": 1,
  "name": "Zoe Carter",
  "role": "user"
 },
 {
  "id": 2,
  "name": "Liam Turner",
  "role": "editor"
 },
~ 省略 ~
 {
  "id": 20,
  "name": "Tina Morgan",
  "role": "admin"
 }
]

JSON(ジェイソン)は、「JavaScript Object Notation」の略で、データを構造的かつ人間にも読みやすい形式で表現するための軽量なデータ記述言語です。
もともとはJavaScriptの文法をベースにしていますが、現在ではほとんどのプログラミング言語で扱うことができる汎用的なフォーマットとして広く使われています。

Webページやアプリケーションがサーバーとデータをやり取りする際、JSONはその通信の中身を担うことが多く、特にAPI(アプリケーション・プログラミング・インターフェース)との連携において欠かせない存在です。
たとえば、ユーザー情報や設定、検索結果などをサーバーから受け取るとき、JSON形式で送られてくることで、クライアント側のJavaScriptが簡単にそのデータを解析し、画面に反映することができます。

JSONの特徴は、シンプルで直感的な構造にあります。
データは「キー」と「値」のペアで構成され、オブジェクト(中括弧)や配列(角括弧)を使って入れ子構造を作ることもできます。
この構造により、複雑な情報を整理して表現することが可能になります。
また、XMLのようなタグが不要で、記述量が少なく済むため、通信の効率も高く、読み書きの負担が軽減されます。

Web開発においてJSONは、静的なデータの記述だけでなく、動的なコンテンツの生成や状態管理にも活用されており、現代のフロントエンド・バックエンドの橋渡し役として、非常に重要な役割を果たしています。

Javascriptの記載例

このスクリプトは、外部のJSONデータを読み込み、ユーザー一覧を動的に「名前検索」「役割フィルタ」「ソート」「役割ごとの人数集計」を行い、その結果をHTML上に描画する一連の流れを実装しています。

データの取得からUIの更新、イベントリスナーの設定までが明確に分割されており、各処理を責務ごとに関数化して保守性を高めています。


// データ取得元
const DATA_URL = '/contents/nosql/feinusers.json';

// DOM要素の取得
const nameInput = document.getElementById('search-name');
const roleSelect = document.getElementById('role-filter');
const sortSelect = document.getElementById('sort-order');
const userList = document.getElementById('fein-user-list');
const roleCount = document.getElementById('role-count');

let users = []; // 外部JSONから読み込んだデータを保持

// JSONを読み込む
fetch(DATA_URL)
 .then(response => {
  if (!response.ok) throw new Error('読み込み失敗: ' + response.status);
  return response.json();
 })
 .then(data => {
  users = data;
  updateView(); // 初期表示
 })
 .catch(error => {
  userList.textContent = 'ユーザーデータの読み込みに失敗しました';
  console.error(error);
 });

// 名前による部分一致検索
function filterByName(keyword) {
 return users.filter(user =>
  user.name.toLowerCase().includes(keyword.toLowerCase())
 );
}

// 役割によるフィルタ
function filterByRole(role, list) {
 if (!role) return list;
 return list.filter(user => user.role === role);
}

// ソート処理
function sortUsers(list, order) {
 const sorted = [...list]; // 元の配列を破壊しないようコピー
 switch (order) {
  case 'id-asc':
   sorted.sort((a, b) => a.id - b.id);
   break;
  case 'id-desc':
   sorted.sort((a, b) => b.id - a.id);
   break;
  case 'name-asc':
   sorted.sort((a, b) => a.name.localeCompare(b.name));
   break;
  case 'name-desc':
   sorted.sort((a, b) => b.name.localeCompare(a.name));
   break;
 }
 return sorted;
}

// 役割ごとの人数集計
function countRoles(filteredUsers) {
 return filteredUsers.reduce((acc, user) => {
  acc[user.role] = (acc[user.role] || 0) + 1;
  return acc;
 }, {});
}

// ユーザー一覧を描画
function renderUserList(filteredUsers) {
 userList.innerHTML = '';
 if (filteredUsers.length === 0) {
  userList.textContent = '該当するユーザーが見つかりません';
  return;
 }

 const ul = document.createElement('ul');
 filteredUsers.forEach(user => {
  const li = document.createElement('li');
  li.textContent = `ID: ${user.id} / 名前: ${user.name} / 役割: ${user.role}`;
  ul.appendChild(li);
 });
 userList.appendChild(ul);
}

// 集計結果を描画
function renderRoleCount(filteredUsers) {
 const counts = countRoles(filteredUsers);
 roleCount.textContent = `Admins: ${counts.admin || 0}, Editors: ${counts.editor || 0}, Users: ${counts.user || 0}`;
}

// 検索・フィルタ・ソートの更新処理
function updateView() {
 const keyword = nameInput.value.trim();
 const role = roleSelect.value;
 const sortOrder = sortSelect.value;

 let result = users;
 if (keyword) result = filterByName(keyword);
 result = filterByRole(role, result);
 result = sortUsers(result, sortOrder);

 renderUserList(result);
 renderRoleCount(result);
}

// イベントリスナーの設定
nameInput.addEventListener('input', updateView);
roleSelect.addEventListener('change', updateView);
sortSelect.addEventListener('change', updateView);

では、各パートを順に詳しく解説していきます。

データ取得元の定義

const DATA_URL = '/contents/nosql/feinusers.json';

目的
外部JSONファイルのパスを定数として定義。
メリット
URLをハードコードせずに定数化することで、後から変更しやすく、保守性が向上します。
補足
このファイルはユーザー情報(ID、名前、役割など)を含むと想定され、fetch() で非同期に読み込まれます。
DOM要素の取得

const nameInput = document.getElementById('search-name');
const roleSelect = document.getElementById('role-filter');
const sortSelect = document.getElementById('sort-order');
const userList = document.getElementById('fein-user-list');
const roleCount = document.getElementById('role-count');

目的:HTML内の操作対象となる要素を変数に格納。

役割ごとの説明:
nameInput:検索キーワード入力欄
roleSelect:役割フィルタのセレクトボックス
sortSelect:並び順のセレクトボックス
userList:ユーザー一覧の表示領域
roleCount:役割ごとの人数集計の表示領域

ベストプラクティス:IDで取得することで、明確かつ高速にアクセス可能。
このようにセマンティックな命名がされていると、コードの可読性も高まります。

ユーザーデータの保持用変数

let users = []; // 外部JSONから読み込んだデータを保持

目的
fetch() で取得したユーザーデータを格納するための変数。
スコープ
グローバルに定義されているため、他の関数(フィルタや描画など)からもアクセス可能。
注意点
let を使うことで、後から users = data のように再代入できるようになっています。
データ取得の仕組み

まず fetch(DATA_URL) を使って外部JSONを取得します。


fetch(DATA_URL)
  .then(response => {
    if (!response.ok) {
      throw new Error('読み込み失敗: ' + response.status);
    }
    return response.json();
  })
  .then(data => {
    users = data;
    updateView();
  })
  .catch(error => {
    userList.textContent = 'ユーザーデータの読み込みに失敗しました';
    console.error(error);
  });

ここでは、HTTPステータスをチェックし、JSONへのパースに成功したら users に格納、失敗時にはエラーメッセージを画面に表示してコンソールに詳細を出力します。

名前による部分一致検索

ユーザー名を小文字化して部分一致検索を行うのが filterByName 関数です。


function filterByName(keyword) {
  return users.filter(user =>
    user.name.toLowerCase().includes(keyword.toLowerCase())
  );
}

includes を使うことで、入力した文字列がユーザー名のどこかに含まれていればマッチし、柔軟な検索体験を実現しています。

役割フィルタの実装

役割フィルタでは、選択肢が空文字なら全リストを返し、特定の役割が選ばれたらその役割だけを抽出します。


function filterByRole(role, list) {
  if (!role) return list;
  return list.filter(user => user.role === role);
}

空文字チェックで「フィルタなし」も処理できるため、一覧表示とフィルタ表示をシンプルに切り替えています。

ソート処理の仕組み

ID昇順・降順、名前昇順・降順の4パターンに対応した sortUsers 関数は、元の配列を壊さないようコピーしてソートしています。


function sortUsers(list, order) {
  const sorted = [...list]; // 元の配列を破壊しないようコピー

  switch (order) {
    case 'id-asc':
      sorted.sort((a, b) => a.id - b.id);
      break;
    case 'id-desc':
      sorted.sort((a, b) => b.id - a.id);
      break;
    case 'name-asc':
      sorted.sort((a, b) => a.name.localeCompare(b.name));
      break;
    case 'name-desc':
      sorted.sort((a, b) => b.name.localeCompare(a.name));
      break;
  }

  return sorted;
}

localeCompare を活用することで日本語や特殊文字にも適切に対応できます。

役割ごとの人数集計

選別後のユーザーリストを reduce で集計し、役割ごとの人数をオブジェクト形式で得るのが countRoles 関数です。


function countRoles(filteredUsers) {
  return filteredUsers.reduce((acc, user) => {
    acc[user.role] = (acc[user.role] || 0) + 1;
    return acc;
  }, {});
}

集計結果は空のオブジェクトからスタートし、各ユーザーの役割をカウントアップしていきます。

ユーザー一覧の描画

renderUserList 関数では、フィルタ・ソート済みのユーザー配列を `<ul> <li>` リストに変換してDOMへ挿入します。


function renderUserList(filteredUsers) {
  userList.innerHTML = '';

  if (filteredUsers.length === 0) {
    userList.textContent = '該当するユーザーが見つかりません';
    return;
  }

  const ul = document.createElement('ul');

  filteredUsers.forEach(user => {
    const li = document.createElement('li');
    li.textContent = `ID: ${user.id} / 名前: ${user.name} / 役割: ${user.role}`;
    ul.appendChild(li);
  });

  userList.appendChild(ul);
}

要素を再生成することで、古いリストを一掃して最新の状態だけを表示できます。

役割集計結果の描画

renderRoleCount では、集計オブジェクトから各役割の人数を取り出してテキスト化し、表示エリアにセットします。


function renderRoleCount(filteredUsers) {
  const counts = countRoles(filteredUsers);
  roleCount.textContent = `Admins: ${counts.admin || 0}, Editors: ${counts.editor || 0}, Users: ${counts.user || 0}`;
}

未出現の役割にはデフォルトで0を表示することで、UIが崩れないよう工夫しています。

表示更新の流れ

updateView 関数は、検索キーワード、役割フィルタ、ソート順を取得し、以下の手順で処理を組み合わせます。

  1. 名前フィルタ
  2. 役割フィルタ
  3. ソート
  4. ユーザーリスト描画
  5. 役割集計描画

function updateView() {
  const keyword = nameInput.value.trim();
  const role = roleSelect.value;
  const sortOrder = sortSelect.value;

  let result = users;

  if (keyword) {
    result = filterByName(keyword);
  }

  result = filterByRole(role, result);
  result = sortUsers(result, sortOrder);

  renderUserList(result);
  renderRoleCount(result);
}

この一連の流れを通じて、UIは常に最新の条件に沿った情報を反映します。

イベントリスナー設定

最後に、入力フォームやセレクトボックスに対して updateView を呼び出すイベントリスナーを設定しています。


nameInput.addEventListener('input', updateView);
roleSelect.addEventListener('change', updateView);
sortSelect.addEventListener('change', updateView);

ユーザー操作が発生するたびに表示が更新されるため、リアルタイムなフィードバックが得られます。

小さなデータベースから広がる可能性

今回のページでは、JSONJavaScriptを活用して、簡易的なNoSQL風データベースを構築する手法を紹介しました。
静的なデータを扱いながらも、検索・フィルタ・ソート・集計といった基本的なデータ操作を実装することで、 動的なWebアプリケーションのような体験を提供できることを確認しました。

このアプローチは、個人サイト制作者にとって非常に有益です。
なぜなら、サーバーサイドの知識や環境が整っていなくても、クライアントサイドだけでインタラクティブな機能を実現できるからです。
特にポートフォリオサイトや小規模な情報管理ツール、教育用デモなどでは、今回のような構成が軽量かつ柔軟に機能します。

ページ内では以下のような構成で、段階的に機能を組み立てていきました:

これらの構成は、セマンティックなHTML、アクセシビリティ、保守性、そしてセキュリティを意識した設計にもつながります。
たとえば、includeslocaleCompare を使った柔軟な文字列処理は、日本語や特殊文字を扱う際にも有効ですし、 配列のコピーによる非破壊的なソートは、データの安全性を保つ上で重要な配慮です。

今後、より本格的なバックエンド(FlaskMongoDBなど)へと発展させる際にも、今回のような構成は良い足場になるのですよ?
関数の責務分離、表示とロジックの分離、データ構造の明確化といった設計の流れは、いろんなプロジェクトに通用すると思いますね。


サイトマップ

全ページをリスト化したサイトマップも用意していますが、けっこうなページ数があります。
下記の「カテゴリー分けサイトマップ」のほうが使いやすいでしょう。

アナザーエデン関連ページ・サイトマップ

アナザーエデンの強敵戦やストーリーコンテンツのリスト、お勧めバッジなどを掲載したコーナーです。
期間限定のない普通のRPGですので、初心者でも安心して続けていけるゲームとなっています。
もっとも重要なグラスタについては、場所別に網羅した表があります。

個人サイトのホスティングとコンテンツ作成

個人でウェブサイトを作るにはどうすればいいか。
HTML・CSS・JavaScriptの書き方はもちろん、無料かつ広告なしでホームページを作る方法を掲載したコーナーです。
Webデザインやレイアウトについても書いてあります。

魚釣りなどアウトドアのエリア

ゲームとパソコンだけじゃなく、アウトドアも趣味なんです。
このコーナーでは魚釣りの記録とか、魚料理のレシピ、はたまたサイクリングなどなど。
アウトドアに関連するコンテンツが詰め込まれています。