Vuex ORM –フロントエンドのORMと、データのノーマライズ– | Vue.js v-tokyo Meetup #9

Vuex ORM –フロントエンドのORMと、データのノーマライズ– | Vue.js v-tokyo Meetup #9

僕が開発しているVuexにORMライクなアクセスを提供するVuex Plugin、“Vuex ORM”のエントリー向けデックです。Vuex ORMとは何か、なぜ必要か、どうやって使うかをご覧いただけます。

5d98263d9a54a65e9e6517dd490f59ee?s=128

Kia King Ishii

April 24, 2019
Tweet

Transcript

  1. ϑϩϯτΤϯυͷ03.ͱ σʔλͷϊʔϚϥΠζ Kia Ishii Vue.js v-tokyo Meetup #9

  2. ੴҪًѥ Kia King Ishii ໌Δ͘ɺָ͘͠ɺָ؍ओ͕ٛϞοτʔͷ σβΠφʔ/σϕϩούʔ Skills Vue / Laravel

  3. 1SJODJQBM5FDI5BMFOU IUUQTHMPCBMCSBJOTDPN

  4. 7VFY03.ͱ͸ʁ

  5. 7VFY03.ͱ͸ʁ w σʔλΛʮϊʔϚϥΠζʯ͢Δɻ w 7VFY4UPSF΁03.ϥΠΫͳΞΫηεΛఏڙɻ w -BSBWFM&MPRVFOU΍"DUJWF3FDPSEͷΑ͏ͳ"1*ɻ

  6. ͬ͘͟ΓͲΜͳײ͔͡

  7. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } import { Model } from '@vuex-orm/core' class Post extends Model { static entity = 'posts' static fields () { return { id: this.attr(null), user_id: this.attr(null), title: this.string(''), user: this.belongsTo(User, 'user_id') } }
  8. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } Ϟσϧͷ໊લɻ 7VFY4UBUFͷΩʔ໊ͱͳΔɻ
  9. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } ϞσϧͷϑΟʔϧυɻ ͜ͷεΩʔϚʹԊͬͯσʔλ͕ ੜ੒͞ΕΔɻ
  10. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } ϦϨʔγϣϯɻ 6TFS͸ෳ਺ͷ1PTUΛ࣋ͭɻ AVTFS@JEA͕֎෦ΩʔͱͳΔɻ
  11. %BUBCBTFʹ.PEFMΛొ࿥͢Δ import { Database } from '@vuex-orm/core' import User from

    '@/models/User' import Post from '@/models/Post' const database = new Database() database.register(User) database.register(Post)
  12. 7VFYʹΠϯετʔϧ͢Δ import Vuex from 'vuex' import VuexORM from '@vuex-orm/core' import

    database from '@/database' const store = new Vuex.Store({ plugins: [VuexORM.install(database)] })
  13. σʔλΛอଘ͢Δ // Postͷσʔλ const post = { id: 1, user_id:

    1, title: 'Hello, world!', user: { id: 1, name: 'John Doe' } }
  14. σʔλΛอଘ͢Δ // Postͷσʔλ const post = { id: 1, user_id:

    1, title: 'Hello, world!', user: { id: 1, name: 'John Doe' } } ϦϨʔγϣϯɻ 1PTUͷதʹ6TFS͕ωετ͞Ε͍ͯΔɻ
  15. σʔλΛอଘ͢Δ // Postͷσʔλ const post = { id: 1, user_id:

    1, title: 'Hello, world!', user: { id: 1, name: 'John Doe' } } import Post from '@/models/Post' Post.insert({ data: post })
  16. 7VFY4UPSFͷத਎ // Vuex StoreͷState { posts: { data: { 1:

    { id: 1, user_id: 1, title: 'Hello, world!' } } }, users: { data: { 1: { id: 1, name: 'John Doe' } } } }
  17. 7VFY4UPSFͷத਎ // Vuex StoreͷState { posts: { data: { 1:

    { id: 1, user_id: 1, title: 'Hello, world!' } } }, users: { data: { 1: { id: 1, name: 'John Doe' } } } } 1PTUͱ6TFSΛ෼཭ϊʔϚϥΠζ
  18. σʔλͷऔಘ import Post from '@/models/Post' // શͯͷPostΛऔಘ Post.all() // PublishedͳPostΛશͯऔಘ

    Post.query().where('published', true).get() // UserΛҰॹʹऔಘ Post.query().with('user').get()
  19. 7VFY03. w σʔλΛʮϊʔϚϥΠζʯ͢Δɻ w 7VFY4UPSF΁03.ϥΠΫͳΞΫηεΛఏڙɻ w -BSBWFM&MPRVFOU΍"DUJWF3FDPSEͷΑ͏ͳ"1*ɻ

  20. ͳͥඞཁͳͷ͔ʁ

  21. ՝୊ ಉ͡σʔλ͕ࢄΒ͹Δ

  22. None
  23. 6TFS 6TFS

  24. // User͕ωετ͞ΕͯΔ { id: 1, title: 'Star Vuex ORM repository',

    assignee: { id: 1, name: 'Jane Doe' } }
  25. ՝୊  ॏෳͨ͠σʔλΛશͯߋ৽͢Δͷ͸ࠎ͕ંΕΔɻ

  26. ՝୊  ॏෳͨ͠σʔλΛશͯߋ৽͢Δͷ͸ࠎ͕ંΕΔɻ  ωετͨ͠σʔλ͸ѻ͏ͷ͕େมɻ

  27. ՝୊  ॏෳͨ͠σʔλΛશͯߋ৽͢Δͷ͸ࠎ͕ંΕΔɻ  ωετͨ͠σʔλ͸ѻ͏ͷ͕େมɻ  ͳΜ͔ؾ࣋ͪѱ͍ɻ

  28. ͳΜ͔ؾ࣋ͪѱ͍

  29. Ͳ͏͢Δʁ

  30. 7VFYΛ%#ͱͯ͠ѻ͏

  31. 7VFYΛ%#ͱͯ͠ѻ͏ // User { users: { 1: { id: 1,

    name: 'John Doe' }, 2: { id: 2, name: 'Jane Doe' } } } // Todo { todos: { 1: { id: 1, user_id: 1, title: '...' }, 2: { id: 2, user_id: 2, title: '...' } } }
  32. 7VFYΛ%#ͱͯ͠ѻ͏ // User { users: { 1: { id: 1,

    name: 'John Doe' }, 2: { id: 2, name: 'Jane Doe' } } } // Todo { todos: { 1: { id: 1, user_id: 1, title: '...' }, 2: { id: 2, user_id: 2, title: '...' } } }
  33. ͍͍ͷ͔ʁ

  34. None
  35. l/PSNBMJ[JOH4UBUF4IBQFz l6QEBUJOH/PSNBMJ[FE%BUBz

  36. l5IFSFDPNNFOEFEBQQSPBDIUP NBOBHJOHSFMBUJPOBMPSOFTUFEEBUBJO B3FEVYTUPSFJTUPUSFBUBQPSUJPOPG ZPVSTUPSFBTJGJUXFSFBEBUBCBTFz — Redux Documentation

  37. None
  38. 3FEVY03.

  39. None
  40. ͍͍ͷͩ

  41. None
  42. Ͳ͏΍ͬͯ࢖͏͔

  43. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } }
  44. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } Ϟσϧͷ໊લɻ 7VFY4UBUFͷΩʔ໊ͱͳΔɻ
  45. .PEFMΛఆٛ͢Δ // Vuex Store State { entities: { users: {

    data: {} } } } σϑΥϧτͰ͸AFOUJUJFTAΩʔ഑Լʹ શͯͷϞσϧ͕ొ࿥͞ΕΔɻ
  46. .PEFMΛఆٛ͢Δ // Vuex Store State { entities: { users: {

    data: {} }, posts: { data: {} } } } Ϟσϧ͕૿͑ΔͱΩʔ͕૿͑Δɻ
  47. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } ϞσϧͷϑΟʔϧυɻ ͜ͷεΩʔϚʹԊͬͯσʔλ͕ ੜ੒͞ΕΔɻ
  48. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } 5ZQF"UUSJCVUFɻ ϦϨʔγϣϯͰ͸ͳ͍ɺϑΟʔϧυͷܕɻ
  49. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } 5ZQF"UUSJCVUFͷछྨ UIJTBUUS  UIJTTUSJOH  UIJTOVNCFS  UIJTCPPMFBO  UIJTJODSFNFOU
  50. .PEFMΛఆٛ͢Δ // ݩσʔλ { id: 1, age: 30 } //

    อଘ͞Εͨޙͷσʔλ { id: 1, name: '' } .PEFMͰఆ͍ٛͯ͠ͳ͍ϑΟʔϧυ͸ແࢹ͞ΕΔɻ .PEFMͰఆٛ͞Ε͍ͯΔ͕ଘࡏ͠ͳ͍ϑΟʔϧυ͸ σϑΥϧτ஋Ͱੜ੒͞ΕΔɻ
  51. .PEFMΛఆٛ͢Δ class User extends Model { static entity = 'users'

    static fields () { return { id: this.attr(null), name: this.string(''), active: this.boolean(true) } } }
  52. .PEFMΛఆٛ͢Δ // ݩσʔλ { id: 1, name: 'John Doe', active:

    1 } // อଘ͞Εͨޙͷσʔλ { id: 1, name: 'John Doe’, active: true }
  53. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } 3FMBUJPO"UUSJCVUFɻ ϞσϧͷϦϨʔγϣϯΛఆٛ͢Δܕɻ
  54. .PEFMΛఆٛ͢Δ import { Model } from '@vuex-orm/core' class User extends

    Model { static entity = 'users' static fields () { return { id: this.attr(null), name: this.string(''), posts: this.hasMany(Post, 'user_id') } } } 3FMBUJPO"UUSJCVUF UIJTIBT0OF  UIJTCFMPOHT5P  UIJTIBT.BOZ  UIJTIBT.BOZ#Z  UIJTIBT.BOZ5ISPVHI  UIJTCFMPOHT5P.BOZ  UIJTNPSQI0OF  UIJTNPSQI.BOZ  UIJTNPSQI5P  UIJTNPSQI5P.BOZ  UIJTNPSQIFE#Z.BOZ
  55. .PEFMΛఆٛ͢Δ class User extends Model { static entity = 'users'

    static primaryKey = 'userId' static fields () { return { userId: this.attr(null), name: this.string('') } } } ϓϥΠϚϦΩʔ͸มߋՄೳɻ
  56. .PEFMΛఆٛ͢Δ class RoleUser extends Model { static entity = 'role_user'

    static primaryKey = ['roleId', 'userId'] static fields () { return { roleId: this.attr(null), userId: this.attr(null) } } } ෳ߹ΩʔʢίϯϙδοτΩʔʣʹ΋ ରԠՄೳɻ
  57. .PEFMΛఆٛ͢Δ class User extends Model { static entity = 'users'

    static fields () { return { id: this.attr(null), name: this.string(''), role: this.string('') } } isAdmin () { return this.role === 'admin' } } ϞσϧͳͷͰ"DDFTTPSͷఆٛ΋ࣗ༝ࣗࡏɻ user.isAdmin()
  58. .PEVMFͷఆٛ export function state () { return { count: 0

    } } export const mutations = { async increment (state) { state.count++ } }
  59. .PEFMΛఆٛ͢Δ // Vuex Store State { entities: { users: {

    count: 0, data: {} } } } ޷͖ͳ7VFY.PEVMFͷػೳΛ௥ՃՄೳɻ
  60. %BUBCBTFʹ.PEFMΛొ࿥͢Δ import { Database } from '@vuex-orm/core' import User from

    '@/models/User' import Post from '@/models/Post' const database = new Database() database.register(User) database.register(Post)
  61. %BUBCBTFʹ.PEFMͱ.PEVMFΛొ࿥͢Δ import { Database } from '@vuex-orm/core' import User from

    '@/models/User' import users from '@/modules/users' import Post from '@/models/Post' const database = new Database() database.register(User, users) database.register(Post) .PEVMFΛొ࿥͢Δ৔߹͸ୈೋҾ਺ʹ౉͢ɻ
  62. 7VF$PNQPOFOUͰͷ࢖͍ํ <ul> <li :key="user.id" v-for="user in users" > {{ user.name

    }} </li> </ul> import User from '@/models/User' export default { computed: { users () { return User.all() } } } Template Script
  63. import User from '@/models/User' export default { methods: { addUser

    () { User.new() } } } <button @click="addUser"> ADD USER </button> 7VF$PNQPOFOUͰͷ࢖͍ํ Template Script
  64. σʔλͷ࡞੒ɾอଘ const data = { id: 1, user_id: 1, title:

    'Hello, world!', user: { id: 1, name: 'John Doe' } } Post.insert({ data }) .PEFMϝιου͕ར༻Մೳɻ
  65. σʔλͷ࡞੒ɾอଘ const data = { id: 1, user_id: 1, title:

    'Hello, world!', user: { id: 1, name: 'John Doe' } } this.$store.dispatch('entities/users/insert', { data }) 7VFY.PEVMF΋ར༻Մೳɻ
  66. σʔλͷ࡞੒ɾอଘ // Vuex StoreͷState { posts: { data: { 1:

    { id: 1, user_id: 1, title: 'Hello, world!' } } }, users: { data: { 1: { id: 1, name: 'John Doe' } } } }
  67. None
  68. /PSNBMJ[Sͷ࢖͍ํ import { normalize, schema } from 'normalizr' // UserΛఆٛɻ

    const user = new schema.Entity('user') // PostΛఆٛɻ const post = new schema.Entity('post', { user }) // σʔλΛϊʔϚϥΠζɻ normalize(data, post)
  69. σʔλͷ࡞੒ɾอଘ Post.insert() Post.create() Post.new() Post.update() Post.insertOrUpdate() Ϩίʔυͷ௥ՃɻID͕ॏෳͯͨ͠Β্ॻ͖ɻ طଘͷϨίʔυΛશͯ࡟আͯ͠ϨίʔυΛ௥Ճɻ શͯσϑΥϧτ஋ͷϨίʔυΛ௥Ճɻ ϨίʔυΛߋ৽ɻ

    Ϩίʔυ͕ଘࡏ͢Ε͹ߋ৽ɻͳ͚Ε͹৽ن௥Ճɻ
  70. σʔλͷߋ৽ Post.update({ where: 1, data: { title: 'Modified title.' }

    }) JE͕ͷ1PTUͷUJUMFΛߋ৽ɻ
  71. σʔλͷߋ৽ Post.update({ where (post) { return post.published }, data: {

    published: false } }) XIFSF͸ΫϩʔδϟΛࢦఆՄೳɻ ෳ਺ͷϨίʔυΛߋ৽͢Δ৔߹ʹศརɻ
  72. σʔλͷߋ৽ Post.update({ data: { id: 1, title: 'Modified title.' }

    }) 1SJNBSZ,FZΛEBUBʹؚΊΔܗͰ΋0,ɻ
  73. σʔλͷߋ৽ Post.update({ where: 1, data (post) { post.tags.push('new tag') }

    }) EBUB΋ΫϩʔδϟͰࢦఆՄೳɻ ഑ྻ΍ΦϒδΣΫτͷϑΟʔϧυΛߋ৽ ͍ͨ͠৔߹ʹศརɻ
  74. 7VFY03.͸7VFY

  75. 4UBUFΛ.VUBUFͯ͠͸ ͍͚ͳ͍

  76. ඞͣ7VFYܦ༝ͰσʔλΛߋ৽ post.title = 'New title' Post.update({ where: 1, data: {

    title: 'Modified title.' } }) NO! YES!
  77. σʔλͷ࡟আ Post.delete(1) JE͕ͷ1PTUΛ࡟আ͢Δɻ

  78. σʔλͷ࡟আ Post.delete((post) => { return !post.published }) ΫϩʔδϟͰ΋ࢦఆՄೳɻ 1VCMJTIFE͕GBMTZͳ1PTUΛશͯ࡟আɻ

  79. σʔλͷऔಘ Post.all() [ { id: 1, title: '...' }, {

    id: 2, title: '...' } ] શͯͷ1PTUΛऔಘ͢Δɻ ໭Γ஋͸഑ྻɻ
  80. σʔλͷऔಘ export default { computed: { posts () { return

    Post.all() } } } ݪଇDPNQVUFEϓϩύςΟͷதͰ ݺͼग़͢ɻ
  81. σʔλͷऔಘ export default { computed: { posts () { return

    this.$store .getters['entities/posts/all']() } } } 7VFY(FUUFSTͱͯ͠ݺͼग़ͯ͠΋0,ɻ
  82. σʔλͷऔಘ Post.find(1) JE͕ͷ1PTUΛऔಘɻ ໭Γ஋͸ΦϒδΣΫτɻ

  83. σʔλͷऔಘ const post = Post.find(1) post.hasTag('vuex-orm') औಘͨ͠σʔλ͸ϞσϧͷΠϯελϯεɻ Ϟσϧʹఆٛͨ͠ϝιου͕ར༻Մೳɻ

  84. 2VFSZ#VJMEFS Post.query() .where('published', true) .get() QVCMJTIFE͕USVFͷ1PTU͚ͩΛ શͯऔಘɻ

  85. 2VFSZ#VJMEFS Post.query() .where('published', true) .orWhere('category', 'vue') .get() QVCMJTIFE͕USVFɺ·ͨ͸ΧςΰϦ͕ WVFͷ1PTUΛશͯऔಘɻ

  86. 2VFSZ#VJMEFS Post.query() .where((post) => { return post.published || post.category ===

    'vue' }) .get() ΫϩʔδϟΛ࢖ͬͯ͞ΒʹϫΠϧυʹɻ
  87. 2VFSZ#VJMEFS Post.query() .orderBy('created_at', 'desc') .get() Post.query() .offset(1) .limit(15) .get() PSEFS#ZʹΑΔιʔτɺP⒎TFUɺMJNJUʹ

    ΑΔϖʔδϯάͳͲ΋Մೳɻ
  88. 2VFSZ#VJMEFS Post.query().count() Post.query().max('likes') Post.query().min('likes') Post.query().sum('likes') DPVOUɺNBYͱ͍ͬͨ"HHSFHBUF ϝιου΋ଘࡏɻ

  89. ϦϨʔγϣϯͷϩʔυ Post.query().with('user').first() { id: 1, title: '...', user: { id:

    1, name: 'John Doe' } } XJUIΛ࢖ͬͯϦϨʔγϣϯΛϩʔυ͢Δɻ
  90. ϦϨʔγϣϯͷϩʔυ const post = Post.query() .with('user') .first() post.user.fullName() ϦϨʔγϣϯ΋΋ͪΖΜϞσϧͷ Πϯελϯεɻ

  91. ϦϨʔγϣϯͷϩʔυ <article> <h1>{{ post.title }}</h1> <p> Author: {{ post.user.fullName() }}

    </p> </article> import Post from '@/models/Post' export default { computed () { post () { return Post.query(). .with('user') .where( 'slug', this.$route.params.slug ) .first() } } } Template Script
  92. ϦϨʔγϣϯͷϩʔυ User.query() .has('posts', '>=', 3) .get() 1PTUΛ݅Ҏ্͍࣋ͬͯΔϢʔβͷΈ શͯऔಘɻ

  93. 6TFSΛVQEBUF͢Δͱ͖ɺEP/PU.PEJGZ ϓϩύςΟ͕5SVUIZͳϞσϧ͸VQEBUFΛ Ωϟϯηϧ͢Δɻ class User extends Model { // ...

    beforeUpdate (model) { if (model.doNotModify) { return false } } } -JGFDZDMF)PPL
  94. ৭ʑͳλΠϛϯάͷ)PPL͕ར༻Մೳɻ beforeCreate() afterCreate() beforeUpdate() afterUpdate() beforeDelete() afterDelete() beforeSelect() -JGFDZDMF)PPL

  95. 7VFͱಉ͡ܗͰ 1MVHJO͕࡞੒Մೳɻ const plugin = { install ({ Model },

    options) { Model.prototype.globalMethod = function () { // Logic... } } } VuexORM.use(plugin) 1MVHJOT
  96. None
  97. None
  98. Vuex ORM Search Fuse.jsΛ࢖ͬͨᐆດݕࡧ͕Մೳʹɻ Vuex ORM Soft Delete ιϑτσϦʔτ͕Մೳʹɻ Vuex

    ORM Change Flags Ϟσϧʹมߋ͕͔͋ͬͨͲ͏͔ʢisDirtyʣΛ൑ผՄೳʹɻ 0⒏DJBM1MVHJOT
  99. Join our Slack IUUQTWVFYPSNTMBDLDPN

  100. ΤϯδχΞɺ୳ͯ͠·͢ʂ IUUQTHMPCBMCSBJOTDPN

  101. Thank you!

  102. ϑϩϯτΤϯυͷ03.ͱ σʔλͷϊʔϚϥΠζ Kia Ishii Vue.js v-tokyo Meetup #9