【リレーションあり】モックデータを生成するツールを作ってみた

# きっかけ

世の中にある、モックデータを作成するツールを調べていたところ、実際のテーブルデータのように、良い具合にリレーションを考慮して、データを作成してくれるツールがなかったので、自分で作ってみました。
今回はそのツールの紹介です。

# 作ったもの

Mock ＋ Python で Mopy という名前にしました。
リポジトリは[こちら](https://github.com/ya-mori/Mopy)です。

Pythonでデータ構造を定義することで、モックデータのInsert文を表示することができます。

``` sample.py
users = Table(
    "users",
    id=PkIntSeed(),
    age=RandIntSeed(min=0, max=10),
    name=RandStrSeed(size=5),
    birth=RandDateSeed()
)

users.create(7).show_sql()
```

`Table` クラスのインスタンスを生成してデータの定義をしていきます。

``` sample.py
users = Table(
    "users", # テーブル名
    # 以下カラムの定義
    id=PkIntSeed(),
    age=RandIntSeed(min=0, max=10),
    name=RandStrSeed(size=5),
    birth=RandDateSeed()
)
```

この時に、カラム名を指定して、データの種類を定義できます。
データの種類は現時点では、以下の種類があります。
これらは `seed.py` 実装することで追加することが可能です。

|       |       | 
| :---: | :---: | 
| PkIntSeed | プライマリーな整数 | 
| RandIntSeed | ランダムな整数 |
| RandStrSeed | ランダムな文字列 | 
| RandBoolSeed | ランダムな真偽値 |
| RandDateSeed | ランダムな日付 |
| EnumSeed | 任意の文字列のランダム | 

また、以下の命令でデータを生成してSQLを出力することができます。

``` sample.py
users.create(7).show_sql()
```

実行結果

``` output.sql
INSERT INTO users(id, age, name, birth) VALUES (1, 53, 'a3X6w', 1953-07-03);
INSERT INTO users(id, age, name, birth) VALUES (2, 74, 'g2bqS', 2045-06-05);
INSERT INTO users(id, age, name, birth) VALUES (3, 32, '5KXmT', 1931-07-02);
INSERT INTO users(id, age, name, birth) VALUES (4, 75, 'YHbwC', 1938-01-14);
INSERT INTO users(id, age, name, birth) VALUES (5, 5, 'jHaTc', 2031-02-13);
INSERT INTO users(id, age, name, birth) VALUES (6, 14, 'CBPfh', 2087-09-26);
INSERT INTO users(id, age, name, birth) VALUES (7, 58, '47tJr', 1931-02-26);
```
指定された構造でSQLを生成できました！

`One To One` のリレーションを定義することもできます。
定義するには、 `Table()__call__(name)` を使って、カラムに設定した、 `seed` を渡します。
以下サンプルです。

``` sample_relation.py
users = Table(
    "users", # テーブル名
    # 以下カラムの定義
    id=PkIntSeed(),
    age=RandIntSeed(min=0, max=10),
    name=RandStrSeed(size=5),
    birth=RandDateSeed()
)

items = Table(
    "items",
    id=PkIntSeed(),
    price=RandIntSeed(min=0, max=100),
    user_id=users("id"), # userテーブルのidを渡す
    user_name=users("name") # userテーブルのnameを渡す
    type=EnumSeed(["ONE", "TWO", "THERE"]),
    isActive=RandBoolSeed()
)

users.create(7).show_sql()
items.create(5).show_sql()
```

実行結果

``` output.py
INSERT INTO users(id, age, name, birth) VALUES (1, 53, 'a3X6w', 1953-07-03);
INSERT INTO users(id, age, name, birth) VALUES (2, 74, 'g2bqS', 2045-06-05);
INSERT INTO users(id, age, name, birth) VALUES (3, 32, '5KXmT', 1931-07-02);
INSERT INTO users(id, age, name, birth) VALUES (4, 75, 'YHbwC', 1938-01-14);
INSERT INTO users(id, age, name, birth) VALUES (5, 5, 'jHaTc', 2031-02-13);
INSERT INTO users(id, age, name, birth) VALUES (6, 14, 'CBPfh', 2087-09-26);
INSERT INTO users(id, age, name, birth) VALUES (7, 58, '47tJr', 1931-02-26);
INSERT INTO items(id, price, user_id, user_name, type, is_active) VALUES (1, 611, 1, 'a3X6w', 'THERE', False);
INSERT INTO items(id, price, user_id, user_name, type, is_active) VALUES (2, 790, 2, 'g2bqS', 'ONE', True);
INSERT INTO items(id, price, user_id, user_name, type, is_active) VALUES (3, 503, 3, '5KXmT', 'THERE', False);
INSERT INTO items(id, price, user_id, user_name, type, is_active) VALUES (4, 919, 4, 'YHbwC', 'TWO', False);
INSERT INTO items(id, price, user_id, user_name, type, is_active) VALUES (5, 451, 5, 'jHaTc', 'ONE', True);
```

`items.user_id` ,  `items.user_name` と `users.id` , `users.name` が 1対1 の関係になっているのが確認できますね。


# 簡単な仕組み
リレーションを維持するロジックは、一般的はキャッシュの仕組みを利用しています。

主なロジックは `Seed.py` に実装してあります。

``` seed.py
class Seed(metaclass=ABCMeta):

    def __init__(self):
        self.cache = dict()

    def get(self, index):
        """
        値を取得します
        """
        value = self.cache.get(index)
        if value is None:
            generated = self.generate()
            self._save(index=index, value=generated)
            return generated
        else:
            return value

    @abstractmethod
    def generate(self):
        """
        値を新たに生成します
        """
        pass

```

ポイントは３つです。

まずは、コンストラクタで `cache` を辞書初期化して、 key-value ストアとして利用しています。

次に、 `get(index)` 経由で `generate()` で値を生成するのですが、
この時に、 `cache` に `index` を `key` として値を保存しています。
もし、値が保存されている`index` を指定して、 `get(index)` が呼ばれた場合は、 `cache` に保存した値をそのまま返すようにします。
これによって、 同じ `index` が指定されば、同じ値を返す事が保証できます。

さいごに、複数テーブルのカラムで同じ `Seed` のインスタンスを参照させるればそれぞれのテーブルで `index` を指定すれば、リレーションを保って、データを生成する事ができます。


# 自分で拡張する

データの種類は、拡張ができるようになっています。
`Seed.py` を継承して、 `generate（）` メソッドを実装するだけです。

今回は、適当な名前をランダムに返すロジックを実装してみます。
`generate` メソッドが呼ばれたら１件データを返すように実装します。

``` MySeed.py

class MyNameSeed(Seed):

    def __init__(self):
        self.list = ["ノゾミ", "ハナヨ", "ハナマル", "カナン"]
        super().__init__()

    def generate(self):
        return random.choice(self.list)

```

これつかって、簡単なテーブルを定義してみます。

``` sample.py
osimen = Table(
    "osimen",
    name=MyNameSeed()
)

osimen.create(5).show_sql()
```

実行結果

``` output.txt
INSERT INTO osimen(name) VALUES ('ハナマル');
INSERT INTO osimen(name) VALUES ('ハナマル');
INSERT INTO osimen(name) VALUES ('ノゾミ');
INSERT INTO osimen(name) VALUES ('ノゾミ');
INSERT INTO osimen(name) VALUES ('ハナヨ');
```

`カナン`がでなかったかw

# さいごに

ツールを作ることによって、色々な発見をする事ができるのは良いなと思いました！ (`__call__()`とかは初めて知りました。)
閃いたら `one-to-many` や、 `many-to-many` が定義できるようにしたいと思います。
少しでもあなたの参考になれば幸いです。