😶‍🌫️

ポケモン図鑑:進化の流れ編

2024/01/28に公開

進化の流れを表すAPIデータを調べて気づいたんですが、再帰構造になっています。イメージとして、フシギソウはまたフシギバナ段階の情報を含んでいる。

残念ながら、Prismaは再帰クエリをサポートしていないが、SQLを直接クエリに入力するという手があるみたい。ただ、再帰的にクエリするには、そもそも再帰的にデータをDBの保管しないといけない。

そこで、再帰関数を使って再帰的なPrisma作成クエリを作ろうと思った。

両方に「再帰」が入っているからといって、同じなわけじゃないよ

Prismaの公式ドキュメンテーションで説明されているように、新しいデータをDBの保管する際、createで繋がりの持っているデータをともに保管できる:

const createUser = await prisma.user.create({ 
  data: {
    email: 'elsa@prisma.io',
    name: 'Elsa Prisma',
    // postsは本来別のモデルになるが、`create`を使って
    // ユーザのIDを受け継ぐ
    posts: {
      create: {
        title: 'Include this post!',
      },
    },
  },
})

そこで、createの中にまた別のcreateオブジェクトを挿入することができれば、データを再帰的に渡せるのではないかと考えた。進化の流れに戻ると:

const createUser = await prisma.evolutionChain.create({ 
  data: {
    ...evolutionChain,
    chain: {
      create: {
        ...evolutionChain.chain,
	// 次の段階
	evolvesTo: {
	  create: {
	    // イーブイみたいに進化の流れは分岐するようなポケモンに対応するため
	    // 実際の`evolvesTo`は配列だが、あくまでイメージとして:
	    ...evolutionChain.chain.evolvesTo,
	    evolvesTo: {
	      create: { ... }
	    },
	  },
	},
      },
    },
  },
})

Prismaの[nested writes]という機能にあたり、どうやら問題ない。ただ、少し扱いにくいので別のデータ形を使おうと思った。

解決方法

再帰構造ではなく、ネストされたデータを全て取り出し配列に変換する。もちろん、親子関係をある程度保つべく、取り出す前にparentIdフィールドに親のIDを入れておく。しかも、再帰関数のままだ!本当によかった。
ちなみに、ID生成はid++で適当に更新している。

最後に配列の要素をPrismaクエリ化し、prismadb.[$transaction]で全てのクエリがともに成功ことを保証する:

prismadb.$transaction([
  prismadb.evolutionChain.create({
    data: evolutionChain,
  }),
  ...chains,
]);

これでDBに進化データを入れたわけなので、次にUI側に表示する。

(普通の)進化の流れ

進化の流れをUIに表したい場合、複数のケースに対応すべきだ:

  1. 普通の流れ:「フシギダネ→フシギソウ→フシギバナ」みたいに、とりあえず一方に進む進化
    フシギダネの進化の流れ
  2. イーブイ:まあ、ご覧ください

分岐する進化

地獄の苦しみそのものだ。ただ、これほどめちゃくちゃになるのはイーブイのみなので、随時に特化したテンプレートを用意すればいい。
(面白いことに、「シャワーズ」の英語名を直訳すると「水蒸気子」になる。あくまで「シャワー」の複数形である「シャワーズ」と比べたら、どちらが一番おかしいかよくわからない)

これに対し、普通の流れだと表示するだけでいい。進化情報を取得には正しいIDを持つEvolution ChainとともにChainincludeする:

const evolutionChain = await prismadb.evolutionChain.findUnique({
  where: { id: species.evolutionChainId },
  include: { chain: true },
});

注意点

上のデータの表示の際、ポケモンと種類のデータとともに表示される。したがって、リレーションの力を最大限に使いこなし、たった一つのクエリで全データを取得する:

// ポケモンモデルより必要になるリレーションが多いため、種類データをベースにクエリする
const species = await prismadb.species.findUnique({
    where: { id },
    include: {
      // 進化の流れを含む
      evolutionChain: {
        include: {
          chain: {
            include: {
	      // 表示するために進化ポケモンの種類も含む
              species: {
                include: {
                  pokemon: true,
                },
              },
            },
          },
        },
      },
      // ポケモンデータも含む
      pokemon: true,
    },
  });

矢印

進化の流れを表示する場合、進化条件を表す矢印と説明も中に挟む。その時、進化の流れを表す配列にreduce魔法をかける。進化条件情報は進化後のポケモンデータと同じオブジェクトに存在することに注意してください:

evolutionChain.chain.reduce((acc, evolution) => {
  if (evolution.trigger) {
    // trigger(「引き金」、進化条件)があれば、これは進化したポケモン
    // したがって、そのポケモンの前に進化条件の詳細(ch)を置いておく
    return [...acc, evolution, evolution.species];
  }
  
  // triggerがなかれば、ベースポケモンなのでパス
  return [...acc, evolution.species];
}, [])

あとはtriggerフィールドがオブジェクトに含まれているかどうかで進化条件の矢印とポケモンを区別する。

const isChain = (el: Chain | Species): el is Chain => Object.prototype.hasOwnProperty.call(el, 'trigger');

if (isChain(el)) {
  return (
    <div key={el.id}>
      <p>{getEvolutionTrigger(el)}</p>
      <Image src='/right-arrow.png' alt='right arrow' width={50} height={50} />
    </div>
  );
}

最終状態

これ以上このプロジェクトに時間を費やすつもりはないので、最後にイーブイの詳細ページの最終状態を残す:

Discussion