normalizrの使い方

こんにちは株式会社SCOUTERでフロントエンドエンジニアをしているhirokinishizawaです。

弊社サービスである「人材紹介会社向けの業務管理システム」SARDINEで新しく無料業務管理ツールをリリースしました。 無料業務管理ツールを開発するにあたり設計段階で複雑にネストされるのがわかっていたのでnormalizrを導入することになりました。 今回このブログでは導入したnormalizrの使い方を書いていきます。

normalizrとは

normalizrはデータを正規化するためのライブラリです。 公開されているAPIでも、自社サービスで作成しているAPIでも複雑にネストされているデータを扱うことが多々あると思います。

複雑にネストされているデータを取り扱うのはなかなか大変ですが正規化する事によりEntity毎にidをキーとしたオブジェクトになるためidで辿ることでデータを取ってくることができます。

開発環境

nuxt: 2.2.0
normalizr: 3.3.0

インストール

yarn add normalizr
or
npm install normalizr

使い方の説明

やりたいこと

冒頭で正規化するとidをキーとしたオブジェクトになりidで辿ることでデータを取ってくることが出来るという話を少ししたと思いますが、実際にどのように使うのかを正規化していないデータをstore/module/denormalize.js、正規化したデータをstore/module/normalize.jsのstateに保管して比較しながら使い方の説明をして行きたいと思います。

実際に正規化していないデータと正規化したデータが以下のようになります。

正規化していないデータ

store/module/denormalize.js

// state.posts
  posts: [
    {
      id: 100,
      text: 'テスト1',
      user: {
        id: 100002,
        name: 'Michael',
      },
      comments: [
        {
          id: 200,
          text: 'コメント1',
          post_id: 100,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
        {
          id: 201,
          text: 'コメント2',
          post_id: 100,
          user: {
            id: 100001,
            name: 'Mary',
          },
        },
      ],
    },
    {
      id: 101,
      text: 'テスト2',
      user: {
        id: 100001,
        name: 'Mary',
      },
      comments: [
        {
          id: 202,
          text: 'コメント3',
          post_id: 101,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
        {
          id: 203,
          text: 'コメント4',
          post_id: 101,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
      ],
    },
  ],

正規化したデータ

store/module/normalize.js

// state.posts
  posts: {
    100: {
      id: 100,
      text: 'テスト1',
      user: 100002,
      comments: [200, 201],
    },
    101: {
      id: 101,
      text: 'テスト2',
      user: 100001,
      comments: [202, 203],
    },
  },
// state.comments
  comments: {
    200: {
      id: 200,
      text: 'コメント1',
      post_id: 100,
      user: 100000,
    },
    201: {
      id: 201,
      text: 'コメント2',
      post_id: 101,
      user: 100001,
    },
    202: {
      id: 202,
      text: 'コメント3',
      post_id: 102,
      user: 100000,
    },
    203: {
      id: 203,
      text: 'コメント4',
      post_id: 103,
      user: 100000,
    },
  },
// state.users
  users: {
    100000: {
      id: 100000,
      name: 'Jone',
    },
    100001: {
      id: 100001,
      name: 'Mary',
    },
    100002: {
      id: 100002,
      name: 'Michael',
    },
  },

このようなデータになるまでの説明をしていきたいと思います。

normalizrを実行してstateに保管する

始めにSchema情報を定義してnormalizrを実行するコードを書きます。


import { normalize, schema } from 'normalizr'

// userを表すスキーマ定義
const user = new schema.Entity('users');

// commentを表すスキーマ定義
// commentの中には `user` というデータがありそこに先ほど定義した`user`をセットします。
const comment = new schema.Entity('comments', {
 user: user
});

// postを表すスキーマを定義します
// postにも`user`というデータがあり先ほどと同じように先ほど定義した`user`をセットします。
// postには`comments`というデータがあり`user`と同じように先ほど定義した`comment`をセットします。
const post = new schema.Entity('posts', { 
  user: user,
  comments: [ comment ]
});

// 引数(data)には正規化していないデータを渡します。
export const normalizePost = (data) => {
  return normalize(data, post)
}

引数のdataには先程の正規化する前のデータを入れることにより以下のような正規化されたデータが生成されます。

results: [100, 101],
entities: {
  posts: {
    100: {
      id: 100,
      text: 'テスト1',
      user: 100002,
      comments: [200, 201],
    },
    101: {
      id: 101,
      text: 'テスト2',
      user: 100001,
      comments: [202, 203],
    },
  },
  comments: {
    200: {
      id: 200,
      text: 'コメント1',
      post_id: 100,
      user: 100000,
    },
    201: {
      id: 201,
      text: 'コメント2',
      post_id: 100,
      user: 100001,
    },
    202: {
      id: 202,
      text: 'コメント3',
      post_id: 101,
      user: 100000,
    },
    203: {
      id: 203,
      text: 'コメント4',
      post_id: 101,
      user: 100000,
    },
  },
  users: {
    100000: {
      id: 100000,
      name: 'Jone',
    },
    100001: {
      id: 100001,
      name: 'Mary',
    },
    100002: {
      id: 100002,
      name: 'Michael',
    },
  },
}

上記のデータの各項目ごとにstateを保管すればデータの保管は完了です。

正規化したデータの使い方

次に先ほどstateにいれた正規化されたデータの使い方を正規化していないデータと比較しながら書いていきます。

正規化していない場合

store/module/denormalize.js

const getters = {
  getPostById: (state) => (postId) => state.posts.filter((post) => {
    return post.id === postId
  })
}

index.vue

<template>
  <div>
// 投稿した内容
    {{post.text}}
// 投稿したユーザー名
    {{post.user.name}}
  </div>
  <div v-for="(comment, key) in post.comments" :key="key">
// コメントテキスト
    {{comment.text}}
// コメントしたユーザー名
    {{comment.user.name}}
  </div>
</template>

<script>
  computed: {
    ...mapGetters({
      getPostById: 'post/getPostById'
    }),
    post() {
      return this.getPostById(100)
    }
  }
</script>

this.getPostById(100)のデータ

    {
      id: 100,
      text: 'テスト1',
      user: {
        id: 100002,
        name: 'Michael',
      },
      comments: [
        {
          id: 200,
          text: 'コメント1',
          post_id: 100,
          user: {
            id: 100000,
            name: 'Jone',
          },
        },
        {
          id: 201,
          text: 'コメント2',
          post_id: 100,
          user: {
            id: 100001,
            name: 'Mary',
          },
        },
      ],
    },

正規化した場合

store/module/normalize.js

const getters = {
  getPostById: (state) => (postId) => state.posts[postId],
  getCommentById: (state) => (commentId) => state.comments[commentId]
  getUserById: (state) => (userId) => state.users[userId]
},

index.vue

<template>
  <div>
// 投稿した内容
    {{post.text}}
// 投稿したユーザー名
    {{contributor.name}}
  </div>
  <div v-for="(comment, key) in post.comments" :key="key">
// コメントテキスト
    {{getCommentById(comment).text}}
// コメントしたユーザー名
    {{getUserById(getCommentById(comment).user).name}}
  </div>
</template>

<script>
  computed: {
    ...mapGetters('store', [
      'getPostById',
      'getCommentById',
      'getUserById',
    ]),
    post() {
      return this.getPostById(100)
    },
    contributor() {
      return this.getUserById(this.post.user)
    }
  }
</script>

this.getPostById(100)のデータ

    {
      100: {
        id: 100,
        text: 'テスト1',
        user: 100002,
        comments: [200, 201],
      },
    },

this.getUserById(this.post.user)のデータ

  100002: {
      id: 100002,
      name: 'Michael',
    },
  },

正規化することによりcommentやuserのように少し複雑にネストされていてもidで取り扱うことがてきるようになります。

まとめ

サンプルデータではあまり複雑にネストされていないので、normalizrの実行するのに少し書くコード量を増やせばidをキーとしたオブジェクトに変換してくれるぐらいと思った人もいるかと思います。ですが普段みなさんが扱っているデータは上記なんかよりもっと複雑にネストされているかと思います。複雑にネストされていてもnormalizrを実行することでデータの階層がなくなりEntity毎にidで辿れるということでロジックがシンプルになり、総合的にみたらコード量も減るというとこが魅力的だなと感じました。

最後に

サービスの成長と共にこれから一緒に切磋琢磨していけるメンバーを増やしていきたいと思っています。

興味のある方は下記からご応募いただくか、hirokinishizawaにご連絡ください!

www.wantedly.com

www.wantedly.com

www.wantedly.com