手把手教你實現json嵌套對象的範式化和反範式化


原文在個人博客中:原文地址 若是文章對您有幫助,您的star是對我最好的鼓勵~前端

在json對象嵌套比較複雜的狀況下,能夠將複雜的嵌套對象轉化成範式化的數據。好比後端返回的json對象比較複雜,前端須要從複雜的json對象中提取數據而後呈如今頁面上,複雜的json嵌套,使得前端展現的邏輯比較混亂。git

特別的,若是咱們使用了flux或者redux等做爲咱們前端的狀態管理機(state對象),經過控制state對象的變化,從而呈現不一樣的視圖層的展現,若是咱們在狀態管理的時候,將state對象範式化,能夠減少state對象操做的複雜性,從而能夠清晰的展現視圖更新的過程。github

  • 什麼是數據範式化和反範式化
  • 數據範式化的實現
  • jest編寫簡單的單元測試

本文的源碼地址爲:源碼地址數據庫


1.什麼是數據範式化

(1)數據範式化的定義

本文不會具體介紹在數據庫中關於範式的定義,廣義的數據範式化,就是除了最外層屬性以外,其餘關聯的屬性用外鍵來引用。json

數據範式化的好處有:能夠減小數據的冗餘redux

(2)數據範式化舉例

好比有一個person對象以下所示:後端

{
  'id':1,
  'name':'xiaoliang',
  'age':20,
  'hobby':[{
    id:30,
    desp:'足球'
  },{
    id:40,
    desp:'籃球'
  },{
    id:50,
    desp:'羽毛球'
  }]
}
複製代碼

在上述的對象中,hobby存在嵌套,咱們將perosn的無嵌套的其餘屬性做爲主屬性,而hobby屬性表示的是須要外鍵來引用的屬性,咱們將id做爲外鍵的名稱,將上述的嵌套對象通過範式化處理能夠獲得:數組

{
  person:{
     '1':{
         'id':1,
         'name':'xiaoliang',
         'age':20,
         'hobby':['30','40','50']
     }
  },
  hobby:{
    '30':{
      id:'30',
      desp:'足球'
    },
    '40':{
      id:'40',
      desp:'籃球',
    },
    '50':{
      id:'50',
      desp:'羽毛球'
    }
  }
}
複製代碼

上述對象就是範式化以後的結果,咱們發現主對象person裏面的hobby屬性中,此時變成了id號組成的數組,經過id做爲外鍵來索引另外一個對象hobby中的具體值。函數

(3)數據範式化的優勢

那麼這樣作到底有什麼好處呢?post

好比咱們如今新增了一我的id爲2:

{
  'id':2,
  'name':'xiaoyu',
  'age':20,
  'hobby':[{
    id:30,
    desp:'足球'
  }]
}
複製代碼

他的興趣還好中一樣包含了足球,那麼若是有複雜嵌套對象的形式,對象變成以下的形式:

[
    {
      'id':1,
      'name':'xiaoliang',
      'age':20,
      'hobby':[{
        id:30,
        desp:'足球'
      },{
        id:40,
        desp:'籃球'
      },{
        id:50,
        desp:'羽毛球'
      }]
    },
    {
      'id':2,
      'name':'xiaoyu',
      'age':20,
      'hobby':[{
        id:30,
        desp:'足球'
      }]
    }
]
複製代碼

上述的這個對象嵌套層級就比較深,好比如今咱們發現hobby中的足球的描述發生了變化,好比:

desp:'足球'——> desp:'英式足球'

若是在上述的嵌套對象中直接改變,咱們須要改變兩處位置,其一是id爲1的person中的id爲30的hobby的desp,另外一處是id爲2處的person的id爲30處的hobby的desp.

這仍是person只有2個實例的狀況,若是person的實例更多,那麼,若是僅僅一個hobby改變,就須要改變多處位置。也就顯得操做比較冗餘。

若是用數據範式化來處理,效果如何呢?,將上述的對象範式化獲得:

{
  person:{
     '1':{
         'id':1,
         'name':'xiaoliang',
         'age':20,
         'hobby':['30','40','50']
     },
     '2':{
        'id':2,
        'name':'xiaoyu',
        'age':30,
        'hobby':[30]
     }
  },
  hobby:{
    '30':{
      id:'30',
      desp:'足球'
    },
    '40':{
      id:'40',
      desp:'籃球',
    },
    '50':{
      id:'50',
      desp:'羽毛球'
    }
  }
}
複製代碼

此時若是一樣的發生了:

***desp:'足球'——>  desp:'英式足球'***
複製代碼

這樣的變化,映射以後只須要改變,hobby被查詢對象:

hobby:{
    '30':{
      id:'30',
      desp:'英式足球'
    },
    ......
}
複製代碼

這樣,不管有多少實例引用了id爲30的這個hobby,咱們修改所引發的操做只須要一處就能到位。

(4)數據範式化的缺點

那麼數據範式化有什麼缺點呢?

一句話能夠歸納數據範式化的缺點:查詢性能低下

從上述範式化後的數據能夠看出:

person:{
 '1':{
     'id':1,
     'name':'xiaoliang',
     'age':20,
     'hobby':['30','40','50']
 },
 '2':{
    'id':2,
    'name':'xiaoyu',
    'age':30,
    'hobby':[30]
 }
}
複製代碼

在上述範式化的數據裏,hobby是經過id來表示,若是要索引每一個id的具體值和對象,好比要到上一層的「hobby」對象中去查詢。而原始的嵌套對象能夠很直觀的展現出來,每個id所對應的hobby對象是什麼。

2.數據範式化的實現(此小節和以後的內容能夠選讀)

下面咱們來嘗試編寫範式化(normalize)和反範式化的函數(denormalize).

函數名稱 函數的具體表示
schema.Entity(name, [entityParams], [entityConfig]) --name爲該schema的名稱
--entityParams爲可選參數, 定義該schema的外鍵,定義的外鍵能夠不存在
--entityConfig爲可選參數,目前僅支持一個參數 定義該entity的主鍵,默認值爲字符串'id'
normalize(data, entity) -- data 須要範式化的數據,必須爲符合schema定義的對象或由該類對象組成的數組
-- entity實例
denormalize (normalizedData, entity, entities) -- normalizedData
-- entity -同上
-- entities 同上

實現數據範式化和反範式化,主要是上面3個函數,下面咱們來一一分析。

本文須要範式化的原始數據爲:

const originalData = {
  "id": "123",
  "author":  {
    "uid": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": {
    total: 100,
    result: [{
        "id": "324",
        "commenter": {
        "uid": "2",
          "name": "Nicole"
        }
      }]
  }
}
複製代碼

(1)schema.Entity

範式化以前必須對嵌套對象進行處理,深層嵌套的狀況下,須要用實體Entity進行解構,層級最深的實體須要首先被定義,而後一層層的解耦到最外層。

該實體的構造方法,接受3個參數,第一個參數name,表示範式化後的對象的屬性的名稱,第二個參數entityParams,表示實體化後,原始的嵌套對象和必定義的實體之間的一一對應關係,第三個參數表示的是 用來索引嵌套對象的主鍵,默認的狀況下,咱們用id來索引。

上述實例的實體化爲:

const user = new schema.Entity('users', {}, {
  idAttribute: 'uid'
})
const comment = new schema.Entity('comments', {
  commenter: user
})
const article = new schema.Entity('articles', {
  author: user,
  comments: {
    result: [ comment ]
  }
});
複製代碼

實體化仍是從最裏層到最外層。而且第三個參數表示索引的主鍵。

如何實現構造方法呢?schema.Entity的實現代碼爲,首先定義一個類:

export default class EntitySchema {
  constructor (name, entityParams = {}, entityConfig = {}) {
    const idAttribute = entityConfig.idAttribute || 'id'
    this.name = name
    this.idAttribute = idAttribute
    this.init(entityParams)
  }
  /**
   * [獲取當前schema的名字]
   * @return {[type]} [description]
   */
  getName () {
    return this.name
  }
  getId (input) {
    let key = this.idAttribute
    return input[key]
  }
  /**
   * [遍歷當前schema中的entityParam,entityParam中可能存在schema]
   * @param  {[type]} entityParams [description]
   * @return {[type]}              [description]
   */
  init (entityParams) {
    if (!this.schema) {
      this.schema = {}
    }
    for (let key in entityParams) {
      if (entityParams.hasOwnProperty(key)) {
        this.schema[key] = entityParams[key]
      }
    }
  }
}
複製代碼

定義一個EntitySchema類,構造方法中,由於entityParams存在嵌套的狀況,所以須要在init方法中遍歷entityParams中的schema屬性。此外爲了定義了獲取主鍵和name名的方法,getName和getId。

(2)normalize(data, entity)

上述就是範式化的函數,接受兩個參數,第一個參數爲原始的須要被範式化的數據,第二個參數爲最外層的實體。一樣在上述例子原始數據被範式化,能夠經過以下方式來實現:

normalize(originData,articles)
複製代碼

上述的例子中,最外層的實體爲articles。

那麼如何實現該範式化,首先考慮到最外層的實體,可能存在嵌套,且最外層實體的對象的屬性值不必定是一個schema實體,也多是數組等結構,所以要分別處理schema實體和非schema實體的狀況:

const flatten = (value, schema, addEntity) => {
  if (typeof schema.getName === 'undefined') {
    return noSchemaNormalize(schema, value, flatten, addEntity)
  }
  return schemaNormalize(schema, value, flatten, addEntity)
}
複製代碼

若是傳入的是一個schema實體:

const schemaNormalize = (schema, data, flatten, addEntity) => {
  const processedEntity = {...data}
  const currentSchema = schema
  Object.keys(currentSchema.schema).forEach((key) => {
    const schema = currentSchema.schema[key]
    const temple = flatten(processedEntity[key], schema, addEntity)
    // console.log(key,temple);
    processedEntity[key] = temple
  })
  addEntity(currentSchema, processedEntity)
  return currentSchema.getId(data)
}
複製代碼

那麼狀況爲遞歸該schema,直到從最外層的schema遞歸到最裏層的schema.

若是傳入的不是一個schema實體:

const noSchemaNormalize = (schema, data, flatten, addEntity) => {
  // 非schema實例要分別針對對象類型和數組類型作不一樣的處理
  const object = { ...data }
  const arr = []
  let tag = schema instanceof Array
  Object.keys(schema).forEach((key) => {
    if (tag) {
      const localSchema = schema[key]
      const value = flatten(data[key], localSchema, addEntity)
      arr.push(value)
    } else {
      const localSchema = schema[key]
      const value = flatten(data[key], localSchema, addEntity)
      object[key] = value
    }
  })
  // 根據判別的結果,返回不一樣的值,能夠是對象,也能夠是數組
  if (tag) {
    return arr
  } else {
    return object
  };
}
複製代碼

若是不是一個實體,那麼分爲是一個對象和是一個數組兩種狀況分別來處理。

最後有一個addEntity,遞歸到裏層,再往外層,獲得對應的schema的name所包含的id,和此id所指向的具體對象。

const addEntities = (entities) => (schema, processedEntity) => {
  const schemaKey = schema.getName()
  const id = schema.getId(processedEntity)
  if (!(schemaKey in entities)) {
    entities[schemaKey] = {}
  }
  const existingEntity = entities[schemaKey][id]
  if (existingEntity) {
    entities[schemaKey][id] = Object.assgin(existingEntity, processedEntity)
  } else {
    entities[schemaKey][id] = processedEntity
  }
}
複製代碼

最後咱們的normalize方法具體爲:

const normalize = (data, schema) => {
  const entities = {}
  const addEntity = addEntities(entities)

  const result = flatten(data, schema, addEntity)
  return { entities, result }
}
複製代碼

(3)denormalize反範式化方法

denormalize反範式化方法,接受3個參數,其中normalizedData 和entities表示範式化後的對象的屬性,而entity表示最外層的實體。

調用的方式爲:

const normalizedData = normalize(originalData, article);
// 還原範式化數據
const {result, entities} = normalizedData
const denormalizedData = denormalize(result, article, entities)
複製代碼

反範式化的具體代碼與範式化類似,就不具體說明,詳情請看源代碼。

3. jest簡單單元測試

直接給出簡單的單元測試代碼:

//範式化數據用例,原始數據
const originalData = {
  "id": "123",
  "author":  {
    "uid": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": {
    total: 100,
    result: [{
        "id": "324",
        "commenter": {
        "uid": "2",
          "name": "Nicole"
        }
      }]
  }
}
//範式化數據用例,範式化後的結果數據
const normalizedData={
  result: "123",
  entities: {
    "articles": {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: {
    	total: 100,
    	result: [ "324" ]
        }
      }
    },
    "users": {
      "1": { "uid": "1", "name": "Paul" },
      "2": { "uid": "2", "name": "Nicole" }
    },
    "comments": {
      "324": { id: "324", "commenter": "2" }
    }
 }
}
//開始測試上述用例下的,範式化結果對比
test('test originalData to normalizedData', () => {
  const user = new schema.Entity('users', {}, {
    idAttribute: 'uid'
  });
  const comment = new schema.Entity('comments', {
    commenter: user
  });
  const article = new schema.Entity('articles', {
    author: user,
    comments: {
      result: [ comment ]
    }
  });
  const data = normalize(originalData, article);
  expect(data).toEqual(normalizedData);
});
//開始測試上述例子,反範式化的結果對比
test('test normalizedData to originalData',()=>{
  const user = new schema.Entity('users', {}, {
    idAttribute: 'uid'
  });
  // Define your comments schema
  const comment = new schema.Entity('comments', {
    commenter: user
  });
  // Define your article
  const article = new schema.Entity('articles', {
    author: user,
    comments: {
      result: [ comment ]
    }
  });
  const data = normalize(originalData, article)
  //還原範式化數據
  const {result,entities}=data;
  const denormalizedData=denormalize(result,article,entities);
  expect(denormalizedData).toEqual(originalData)
})
複製代碼
相關文章
相關標籤/搜索