見出し画像

【Tech Blog】マスタデータの段階的なサーバー管理方法について

当社では大小さまざまなプロジェクトの開発を行なっています。
プロジェクトの規模や種類によって開発の方針やルールも変わってきます。
今回は開発上必ず必要となるマスタデータをどのように取り扱うか、マスタデータの管理方法についてご紹介させていただきます。


■ マスタデータの管理方法

まず、一般的に多く利用されるマスタデータ管理方法には下記のものが上げられるかと思います。

1.クラス定数

クラス内に Const として定義する。

●メリット:
・プログラムの処理に一番近い場所にあり、コードを追っていくだけで内容を確認することができる

●デメリット:
・静的な定義となるため、値の変更はコードの改修 / デプロイが必要となる
・基本的には「定数名=値」の1:1の定義となるため、複数の情報を持ちたい場合は設定が煩雑化する

2.yaml や csv による外部ファイル管理

マスタ情報が記載された yaml や csv といったファイルを用意し、プログラム上からファイルを読み取り参照する。

●メリット:
・ファイルは静的なものとなるため、内容を確認することができる

●デメリット:
・実装方法にもよるが、基本的には静的な定義となるため値の変更にはファイルのデプロイが必要となる
・ファイルの読み込み時の Disk I/O がボトルネックになりやすくキャッシュの対応が必要になる

3.DB における管理

データベース上にマスタデータのテーブルを用意し、レコードを登録する。

●メリット:
・外部より動的に値の追加・変更・削除を行うことができる
・マスタデータのテーブルを結合したクエリを発行することが可能

●デメリット:
・現在設定されている内容が何なのかプログラムのコード上からは確認することが出来ない
・AWS 等のクラウド上のデータベースでは、参照時のクエリ毎に費用が発生し、キャッシュの対応が必要になる


■ ミニマムに実装することのメリット

どの方法の管理を行うべきか?というのはよく議論に上がる話かと思います。
必要となる要件・仕様が決まり切っている場合は特に問題ないですが、要件が決まり切っていなかったり、とりあえずデモとして実現したい、などのケースも多々存在します。
また、アジャイル開発においてはより早く実現・フィードバックをもらいビジネス的価値の最大化を図っていく事が大切になります。
  
例えば、「3.DB での管理」の方法ですが、メリットである「動的な変更が可能」という点は、管理画面などの外部から動的に値を変更する仕組みがセットで実現出来て初めて大きく価値を享受 できるものとなります。
そこまでの要求が存在しない場合、1,2 の「クラス定数」や「 yaml や csv によるファイル管理」の方が素早く用意でき、また影響範囲を小さく留められることから値の変更や機能改修も行いやすく優れている場合が多いです。
 
また構成においても小さく作っていくことはオーバーエンジニアリングの防止にもなります。
後の判断をより迅速に行うためにもアプリケーションで求められている要件を達するミニマムな構成がベストとなるケースが多いのではないでしょうか。


■ 段階を追った移行の流れ

ここでは、過去にデモのアプリケーションから始まり、1→2→3と段階を追って実装を行いリリースまで至ったプロジェクトで採用した実装の流れを紹介させていただきます。
例として、「記事データ( article )」という記事データに「カテゴリ」情報を追加して管理したい。という要件の流れの実装例となります。

1.クラス定数における管理

まず下記のような記事データが存在します。


そこから記事をカテゴリでまとめたいという要件が発生した場合になります。
テーブルとしては、下記のように category_id をカラム追加する形となります。


シンプルに定数で持つ場合は、下記のような持ち方となるかと思います。
カテゴリ情報の定義を Article クラスの定数として記述します。


namespace App\Entity;

class Article {
    public const CATEGORY_A = 1;
    public const CATEGORY_B = 2;
    public const CATEGORY_C = 3;

    public function getCategory()
    {
      return $this->category;
    }

    public function setCategory(int $category)
    {
      $this->category = $category;
    }
}


2.外部ファイルによる管理への移行


name の要素だけを拡張する場合、下記のような定数定義を追加する方法もあります。


public const CATEGORY_B_NAME = "カテゴリーB";
public const CATEGORY_A_NAME = "カテゴリーA";
public const CATEGORY_C_NAME = "カテゴリーC";


しかし、「 name 」以外にもマスタ情報として持ちたい要素が存在することから yaml ファイルで管理する方法に移行しました。

この時の実装の要点としては下記となります。
 
 - データベースにマスタデータを持った時の実装と互換性を持ったメソッドのI/Fにて実装する
 
Repository クラスを介し、他のテーブルと同じようにデータを取り扱うことにより、マスタデータ利用側の実装はマスタデータの保存場所を気にすることなく実装できます。

また、どのデータストアに保存されているかというのは Repository クラスの責務となりデータベースへの移行も容易になります。
この時、初めて Category という存在は Entity クラスとなりました。


namespace App\Entity;

class Category {
  private $id;
  private $name;
  private $description;

  public function getId()
    {
      return $this->id;
    }
    public function setId(string $id)
    {
      $this->id = $id;
    }

    public function setName(string $name)
    {
      $this->name = $name;
    }

    public function getName()
    {
      return $this->name;
    }

    public function setDescription(string $description)
    {
      $this->description = $description;
    }

    public function getDescription()
    {
      return $this->description;
    }
}


リポジトリクラス側は DB で取り扱う場合と同様に実装します。
下記は Symfony での実装例となります。


# app/category.yaml
records:
    - ["id": 1, "name": "カテゴリーA", "description": "カテゴリAの説明"]
    - ["id": 2, "name": "カテゴリーB", "description": "カテゴリBの説明"]
    - ["id": 3, "name": "カテゴリーC", "description": "カテゴリCの説明"]


category.yaml としてマスタ情報を用意します。


# app/services.yaml
services:                                                                                                                                        
    _defaults:                                                                                                                                   
        bind:                                                                                                                                    
            $categoryDataFile: 'app/category.yaml'



Symfony では bind を定義することにより、コントラクタにて渡される変数の中身を定義することが出来ます。
下記は Repository クラスの実装例となります。


# CategoryRepository.php
class CategoryRepository extends ServiceEntityRepository 
    /**
     * __construct
     *
     * @param ManagerRegistry $registry
     * @param string          $categoryDataFile カテゴリマスタデータのyamlファイル
     */
    public function __construct(ManagerRegistry $registry, string $categoryDataFile)
    {
        parent::__construct($registry, Category::class);

        $this->records = Yaml::parseFile($categoryDataFile)['records'];
    }

    /**
     * マスタデータ全件を取得する
     *
     * @return Category[]
     */
    public function findAll(): array
    {
        $result = [];
        foreach ($this->records as $data) {
            $result[] = $this->createEntity($data);
        }

        return $result;
    }

    /**
     * idからEntityを取得する
     *
     * @param int $id
     *
     * @return ?Category
     */
    public function getById(int $id): ?Category
    {
        foreach ($this->records as $data) {
            if ($data['id'] !== $id) {
                continue;
            }

            return $this->createEntity($data);
        }

        return null;
    }

    /**
     * Entityを作成する
     *
     * @param array $data
     *
     * @return Category
     */
    private function createEntity(array $data): Category
    {
        $entity = new Category();
        $entity->setId($data['id']);
        $entity->setName($data['name']);

        return $entity;
    }


また、Symfony ではパラメーターに定義したものは PHP 配列に変換されキャッシュする構造を持っています。
下記のように定義することにより、yaml ファイルの読み込みのコストを削減することが出来ます。
csv ファイルを利用したい場合は、csv から category.yaml ファイルを作成する Command などを実装します。


# app/services.yaml
imports:
    - { resource: 'app/category.yaml' }
services:                                                                                                                                        
    _defaults:                                                                                                                                   
        bind:                                                                                                                                    
            $categoryData: '%category%'
# app/category.yaml
parameters:
    category:
        - ["id": 1, "name": "カテゴリーA", "description": "カテゴリAの説明"]
        - ["id": 2, "name": "カテゴリーB", "description": "カテゴリBの説明"]
        - ["id": 3, "name": "カテゴリーC", "description": "カテゴリCの説明"]
# CategoryRepository.php
    public function __construct(ManagerRegistry $registry, array $categoryData)
    {
        parent::__construct($registry, Category::class);
        $this->records = $categoryData;
    }


3.DB による管理への移行

データベース上にテーブルがあった方が良いと判断されたタイミングで移行します。

DB の移行はI/Fが同じであるため、移行は容易なものとなります。
DB 移行が必要となったリポジトリクラス内のデータ取得先を DB から取得するように書き換えます。
キャッシュ処理を除いたシンプルな実装例は下記となります。


# CategoryRepository.php

// /**
//  * マスタデータ全件を取得する
//  * (元のRepositoryクラスに実装されているため不要)
//  *
//  * @return Category[]
//  */
// public function findAll(): array
// {
//     return $this->findAll();
// }

/**
 * idからEntityを取得する
 *
 * @param int $id
 *
 * @return ?Category
 */
public function getById(int $id): ?Category
{
    return $this->find($id);
}


findAll() は元の Repository クラスに実装されているため、不要になります。
取得に関するコードの変更点は getById()内のみとなり、他のコードの変更なく行うことができました。
全てのマスタデータをデータベース上に移行することなく、必要となるテーブルのみを移行することが出来た形となります。


■ まとめ

今回、マスタデータの管理方法について、段階的に移行していく実装例を紹介させていただきました。

「マスタデータは共通して RDB で用意する」といった方針をとっているプロジェクトも存在するかと思います。

しかし、RDB もマスタデータの保存先として選択肢の一つでしかなく、テキストファイルや定数もシステムによっては有用な選択肢になり得ます。
アプリケーションを柔軟に構築していく手段の一つとして参考になれば幸いです。

セガ エックスディーでは幅広い技術を組み合わせ、今まで培ったゲーミフィケーション要素などを活かして今後も面白いものを生み出し課題解決に役立てていきたいと思います。プロモーションの相談や新しいアイデア、こんな事はできないか?と言った質問もぜひお問い合わせください。


執筆:藤木 直希|株式会社セガ エックスディー

セガ エックスディーで XR 系の R & D やプロダクトの開発など、様々な開発・推進に取り組んでいます。

< 過去執筆記事 >
【Tech Blog】体の角度を基にしたWebAR姿勢検知
【Tech Blog】Web AR の実現例とライブラリについて①
【Tech Blog】Web AR の実現例とライブラリについて②


■ SEGA XD HP:https://segaxd.co.jp/
■ SEGA XD 公式 Twitter:https://twitter.com/SEGAXD_PR
■ SEGA XD 公式 Facebook:https://www.facebook.com/segaxd.fb/
■ CX School 公式 Youtube:https://www.youtube.com/@cxschool
■ SEGA XD 公式 TikTok:https://www.tiktok.com/@segaxd_official