VuexORM

こんにちわformrunを開発している @kkyouhei です。

formrunでは先日ユーザが利用する管理画面をSPA化いたしました。
管理画面ではエンドユーザがフォームから入力してきたデータや、その後のやり取り、メモ、添付ファイルなどデータ同士がいくつも関連しています。
RailsにはActiveRecordという心強いライブラリがありますが、vuexにはState同士のリレーションを提供するような機能はありませんでしたのでvuejs ORMなどで検索したところVuexORMというライブラリがありました。
こちらをプロジェクトに導入して開発したところ開発効率が遥かに向上したのでご紹介させていただきます。

正直VuexORMがなかったら開発期間が1.3倍くらいには伸びていたのではないかという印象です。

Vuex-ORMとは?

Stateにデータ同士のリレーションを提供し、基本的なCRUD操作を提供してくれます。

基本的な使い方

インストール方法

npm install @vuex-orm/core

Modelファイルの作成

Railsと同様にまずはモデルを用意致します。

// user.js
import { Model } from '@vuex-orm/core'

export default class User extends Model {
  // VuexORMのデフォルトではentitiesというネームスペースの下に作成されます
  // この例だと entities/users にユーザレコードのstateが作成されます
  static entity = 'users'

  static primaryKey = 'id'

  // カラムのフィールドを定義します
  static fields () {
    return {
      id: this.attr(null),
      name: this.string(''),
      age: this.number(0)
    }
  }
}

モジュールの作成

次のモジュールファイルを用意します。

// users.js
export default{
  namespaced: true
}

Plugin install

VuexのStoreをインスタンス化する時にインスタンス化したdatabaseをプラグインに登録します。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import VuexORM from '@vuex-orm/core'
import User from './User'
import users from './users'

Vue.use(Vuex)

const database = new VuexORM.Database()
database.register(User, users)

const store = new Vuex.Store({
  plugins: [VuexORM.install(database)]
})

export default store

これでデータを作成する準備が整いました。

APIから取得したデータを追加

サンプルではただのjsオブジェクトを返却していますが、axiosでapiを実行していてもそれほどコードは変わらないです。

api.js
API.fetchUsers(res => {
  store.dispatch("entities/users/create", { data: res });
});

追加したデータを全件取得

取得したいモデルをimportしallメソッドを利用することで参照が可能です。

// App.vue
<template>
  <div id="app">
    <h3>User List</h3>
    <input v-for="user in users" :key="user.id" v-model="user.name" />
  </div>
</template>

<script>
import User from "./user"

export default {
  name: "App",
  computed: {
    users() {
      return User.all();
    }
  }
};
</script>

データを更新する

更新はupdateメソッドを実行します。
update対象の条件設定はidフィールドを指定するか、whereプロパティにIDを指定、whereプロパティに判定用のクロージャを渡す、などがあります。
下記の例は一番シンプルなidフィールドを指定しています。

// App.vue
<template>
  <div id="app">
    <h3>User List</h3>
    <input v-for="user in users" :key="user.id" @keyup.enter="update(user.id, user.name)" v-model="user.name" />
    <input @keyup.enter="insert(newUserName)" v-model="newUserName" />
  </div>
</template>

<script>
import User from "./user"

export default {
  name: "App",
  data() {
    return {
      newUserName: ''
    }
  },
  computed: {
    users() {
      return User.all();
    }
  },
  methods: {
    update(id, value) {
      User.update({id: id, name: value})
    },
    insert(value) {
      User.insert({
          data: {name: value}
      })
      this.newUserName = ''
    }
  }
};
</script>

データを追加する

追加はinsertメソッドを使用します。

// App.vue
<template>
  <div id="app">
    <h3>User List</h3>
    <input @keyup.enter="insert(newUserName)" v-model="newUserName" />
  </div>
</template>

<script>
import User from "./user"

export default {
  name: "App",
  data() {
    return {
      newUserName: ''
    }
  },
  computed: {
    users() {
      return User.all();
    }
  },
  methods: {
    insert(value) {
      User.insert({
          data: {name: value}
      })
      this.newUserName = ''
    }
  }
};
</script>

サンプル

検索

ID検索

id検索はfindを使用して検索を行います。

User.find(1)

whereで複数レコードを取得

年齢が30のレコードを取得

return User.query().where('age', 30).get()

大小比較を行う

年齢が30より大きいレコードを取得

User.query().where('age', age => age > 30).get()

and条件を設定

年齢が20より大きく40未満のレコードを取得

User.query().where('age', age => age > 20)
            .where('age', age => age < 40)
            .get()

or条件を設定

年齢が40以上か20以下のレコードを取得

User.query().where('age', age => age > 39)
            .orWhere('age', age => age < 21)
            .get()

複雑な条件設定

より柔軟な条件の設定には引数にクロージャを設定します。
クロージャの内部でbooleanを返却することでtrueのレコードのみ取得が可能です。

User.query().where(record => {
  return record.name.includes('John') && record.age > 20
}).get()

サンプル

リレーション

現在提供されているリレーションは以下です。

  1. BelongsTo
  2. BelongsToMany
  3. HasMany
  4. HasManyBy
  5. HasManyThrough
  6. HasOne
  7. MorphedByMany
  8. MorphMany
  9. MorphOne
  10. MorphTo
  11. MorphToMany

HasMany BelongsTo

リレーションを定義するにはまずModelクラスにリレーションを定義します。
hasManyの場合は第一引数にリレーション先のモデルクラス、第二引数ではリレーションのフィールドを指定します。

// User.js
import { Model } from "@vuex-orm/core";
import Post from "./post";

export default class User extends Model {
  static entity = "users";
  static primaryKey = "id";

  static fields() {
    return {
      id: this.increment(),
      name: this.string(""),
      age: this.number(0),

      posts: this.hasMany(Post, "user_id")
    };
  }
}
// Post.js
import { Model } from "@vuex-orm/core";
import User from "./user";
import Comment from "./comment";

export default class Post extends Model {
  static entity = "posts";
  static primaryKey = "id";

  static fields() {
    return {
      id: this.increment(),
      title: this.string(""),
      content: this.string(""),
      user_id: this.attr(null),

      author: this.belongsTo(User, "user_id"),
      comments: this.hasMany(Comment, "post_id")
    };
  }
}

定義するだけではgetやfirstで取得した後利用することができません。
withやwithAllRecursiveを呼び出してリレーション先のレコードも取得します。

User.with('posts').all()

HasManyThrough

hasManyThroughでは4つ引数を指定します。
hasManyThrough(最終的に取得するモデル, 中間のモデル, 中間モデルと呼び出しモデルを接続するキー名, 中間モデルと第一引数で指定したモデルを接続するキー)

User.js
import { Model } from "@vuex-orm/core";
import Post from "./post";
import Comment from "./comment";

export default class User extends Model {
  static entity = "users";
  static primaryKey = "id";

  static fields() {
    return {
      id: this.increment(),
      name: this.string(""),
      age: this.number(0),

      posts: this.hasMany(Post, "user_id"),
      comments: this.hasManyThrough(Comment, Post, "user_id", "post_id")
    };
  }
}

サンプル

レコードの追加更新削除

レコードの追加

追加を行うにはinsertメソッドを使用します。
または直接actionを呼ぶことも可能です。

User.insert({data: {id: 1, name: 'user name'} })

store.dispatch('entities/users/create', { data: {name: 'user name'} })

一度に複数レコード追加

複数追加する場合はdataプロパティに配列を渡します。

User.insert({data: [
  {name: 'One user name'},
  {name: 'Two user name'}
]})

レコードの更新

更新を行うにはupdateメソッドを使用します。
IDフィールドを設定することで更新対象を指定できます。

User.update({id: 1, name: 'user name'})

複雑な更新対象の選択

whereと同様にクロージャを渡して更新対象を選択することができます。

// whereプロパティにIDを指定
User.update({
  where: id,
  data: {name: value}
})

// whereプロパティにcrosureを指定して複雑な条件を設定
User.update({
  where: record => record.id === id,
  data: {name: value}
})

レコードの削除

削除を行うにはdeleteメソッドを使用します。
引数には削除対象のIDを設定します。

User.delete(userId)

レコードの追加または更新

追加または更新を行うにはinsertOrUpdateを使用します。
指定したIDが存在していれば更新を行い、存在しなければ新規追加を行います。

User.insertOrUpdate({data: {id: 1, name: 'user name'} })

サンプル

最後に

VueでSPA開発する際にはVuexORMは大変重宝しました。
ただ若いOSSなのでバグがあった時に自分で直してプルリクを送ったり大変な一面もありますがそれ以上に自分で育てていく楽しさがVuexORMにはあります。

まだバグフィックスしか出来ていませんが、saveメソッドを実装するというissueがあるので、こちらを現在こそこそ実装中です。
皆さんもぜひVuexORMを導入して一緒に育てて行きましょう!