【Unity】【MasterMemory】Validatorを使ってマスタデータをバリデーションする

MasterMemoryのValidatorを使ってマスタデータをバリデーションする方法についてまとめました。

はじめに

MasterMemoryを使うと、マスタデータを効率よく取り扱うことができます。

github.com

MasterMemoryの概要と基本的な使い方については当ブログの以下の記事にまとめています。
本記事の内容はこれを前提知識としますので、必要に応じてこちらも参照してください。

light11.hatenadiary.com

さて、マスタデータは人間が入力するものなので、入力に間違いがないかをバリデーションすることは重要です。
MasterMemoryにはバリデーションを行うための仕組みが用意されています。

本記事ではこの使い方についてまとめます。

テスト用のデータ構造を作る

まずテスト用のデータ構造を定義します。
ステージを表すStageMasterクラスと、アイテムを表すItemMasterクラスを作ります。

using System.Linq;
using MasterMemory;
using MessagePack;

[MemoryTable("Stage")]
[MessagePackObject(true)]
public sealed class StageMaster
{
    public StageMaster(string id, string name, string rewardId)
    {
        Id = id;
        Name = name;
        RewardId = rewardId;
    }

    [PrimaryKey] public string Id { get; private set; }

    public string Name { get; }

    public string RewardId { get; }
}
using MasterMemory;
using MessagePack;

[MemoryTable("Item")]
[MessagePackObject(true)]
public sealed class ItemMaster
{
    public ItemMaster(string id, string name)
    {
        Id = id;
        Name = name;
    }

    [PrimaryKey] public string Id { get; }

    public string Name { get; private set; }
}

バリデーションルールを設定する

次にバリデーションルールを設定します。

バリデーションルールを設定するには、以下のように前節のデータ構造定義クラスにIValidatableを実装します。
この例では、IDがstage-で始まっていなかったらエラーになるように設定しています。

using MasterMemory;
using MessagePack;

[MemoryTable("Stage")]
[MessagePackObject(true)]
public sealed class StageMaster : IValidatable<StageMaster> // IValidatableを実装する
{
    public StageMaster(string id, string name, string rewardId)
    {
        Id = id;
        Name = name;
        RewardId = rewardId;
    }

    [PrimaryKey] public string Id { get; }

    public string Name { get; }

    public string RewardId { get; }

    // バリデーション時に各レコードごとにこれが呼ばれる
    void IValidatable<StageMaster>.Validate(IValidator<StageMaster> validator)
    {
                // IDがstage-で始まっていなかったらエラー
        if (!Id.StartsWith("stage-"))
            validator.Fail("Id must start with 'stage-'");
    }
}

バイナリをビルドする

次にバリデーションを実行するために、MasterMemoryのバイナリを作成します。

まずMasterMermoryとMessagePackのコード生成を実行します。
この辺りの詳細は割愛しますが、以下の記事にまとめていますので必要に応じて参照してください。

light11.hatenadiary.com

コード生成を実行したら以下のコードによりバイナリファイルを生成します。

using System.Collections.Generic;
using System.IO;
using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;

public static class BinaryGenerator
{
    [MenuItem("Example/Generate Binary")]
    private static void Run()
    {
        // MessagePackの初期化(ボイラープレート)
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var stages = new List<StageMaster>
        {
            new("stage-01-001", "初心者の森", "item-01-001"),
            new("stage-01-002", "迷いの湿地帯", "item-01-002"),
            new("stg-01-003", "炎の山脈", "item-01-003"),
            new("stage-02-001", "氷結の洞窟", "item-02-001"),
            new("stage-02-002", "幽霊の墓地", "item-02-002"),
            new("stage-02-003", "竜の城塞", "item-02-003")
        };

        var items = new List<ItemMaster>
        {
            new("item-01-001", "木の枝"),
            new("item-01-002", "薬草"),
            new("item-01-003", "鉄の剣"),
            new("item-02-001", "氷の結晶"),
            new("item-02-002", "幽霊の涙"),
            new("item-02-003", "竜の牙")
        };

        // バイナリを作成
        var builder = new DatabaseBuilder();
        builder.Append(stages);
        builder.Append(items);
        var binary = builder.Build();

        // バイナリを永続化
        var path = "Assets/Example/Binary/Example.bytes";
        var directory = Path.GetDirectoryName(path);
        if (!Directory.Exists(directory))
            Directory.CreateDirectory(directory);
        File.WriteAllBytes(path, binary);
        AssetDatabase.Refresh();
    }
}

上部のメニューからExample > Generate Binary を実行するとバイナリファイルが生成されます。

バリデーションを実行する

最後にバリデーションを実行します。

バリデーションを実行するには以下のようにMemoryDatabase.Validate()を実行します。

using Example;
using MessagePack;
using MessagePack.Resolvers;
using UnityEditor;
using UnityEngine;

public static class BinaryValidator
{
    [MenuItem("Example/Validate Binary")]
    private static void Run()
    {
        var messagePackResolvers = CompositeResolver.Create(
            MasterMemoryResolver.Instance,
            GeneratedResolver.Instance,
            StandardResolver.Instance
        );
        var options = MessagePackSerializerOptions.Standard.WithResolver(messagePackResolvers);
        MessagePackSerializer.DefaultOptions = options;

        var path = "Assets/Example/Binary/Example.bytes";
        var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
        var binary = asset.bytes;

        var memoryDatabase = new MemoryDatabase(binary);

        // バリデーションを実行
        var validateResult = memoryDatabase.Validate();
        if (validateResult.IsValidationFailed)
        {
            // 文字列でエラー内容を出力するには FormatFailedResults() を使う
            Debug.LogError(validateResult.FormatFailedResults());

            // エラーの詳細を取得する場合には以下のように書く
            foreach (var failedItem in validateResult.FailedResults)
            {
                var data = (StageMaster)failedItem.Data;
                Debug.LogError(
                    $"[Failed] Type: {failedItem.Type}, Message: {failedItem.Message}, Data: {data.Id}");
            }
        }
        else
        {
            Debug.Log("Validation succeeded without any errors.");
        }
    }
}

Example > Validate Binaryを実行するとバリデーションが実行されます。

今回は1レコードだけバリデーションに引っ掛かるようにしているため、コンソールに以下のエラーが出力されるはずです。

StageMaster - Id must start with 'stage-', PK(Id) = stg-01-003

いろんなバリデーションの書き方

ここまでの例ではバリデーションを失敗させるために、IValidator.Fail()を使いました。
IValidatorにはこれ以外にもさまざまなバリデーションの方法が用意されています。
以下はいろんなバリデーションを行っている例です。

using System.Linq;
using MasterMemory;
using MessagePack;

[MemoryTable("Stage")]
[MessagePackObject(true)]
public sealed class StageMaster : IValidatable<StageMaster>
{
    public StageMaster(string id, string name, string rewardId)
    {
        Id = id;
        Name = name;
        RewardId = rewardId;
    }

    [PrimaryKey] public string Id { get; }

    public string Name { get; }

    public string RewardId { get; }

    void IValidatable<StageMaster>.Validate(IValidator<StageMaster> validator)
    {
        // MemoryDatabase内の他のテーブル(今回はItemMasterのテーブル)を取得
        var items = validator.GetReferenceSet<ItemMaster>();

        // ItemMasterの中に自身のIDが存在するかチェック
        validator.Validate(x => items.TableData.Any(y => y.Id == x.RewardId));
        // このように書くこともできる
        items.Exists(x => x.RewardId, x => x.Id);

        // 以下のようにCallOnceを使うと、全レコードを通して一回だけしか呼ばれない
        if (validator.CallOnce())
        {
            // 全てのレコードを取得
            var quests = validator.GetTableSet();
            // 重複チェックのメソッドなどは用意されている
            quests.Unique(x => x.Name);
        }
    }
}

参考

github.com