手把手教你實現json巢狀物件的正規化化和反正規化化

yuxiaoliang發表於2018-07-02

原文在我的部落格中:原文地址 如果文章對您有幫助,您的star是對我最好的鼓勵~

在json物件巢狀比較複雜的情況下,可以將複雜的巢狀物件轉化成正規化化的資料。比如後端返回的json物件比較複雜,前端需要從複雜的json物件中提取資料然後呈現在頁面上,複雜的json巢狀,使得前端展示的邏輯比較混亂。

特別的,如果我們使用了flux或者redux等作為我們前端的狀態管理機(state物件),通過控制state物件的變化,從而呈現不同的檢視層的展示,如果我們在狀態管理的時候,將state物件正規化化,可以減小state物件操作的複雜性,從而可以清晰的展示檢視更新的過程。

  • 什麼是資料正規化化和反正規化化
  • 資料正規化化的實現
  • jest編寫簡單的單元測試

本文的原始碼地址為:原始碼地址


1.什麼是資料正規化化

(1)資料正規化化的定義

本文不會具體介紹在資料庫中關於正規化的定義,廣義的資料正規化化,就是除了最外層屬性之外,其他關聯的屬性用外來鍵來引用。

資料正規化化的好處有:可以減少資料的冗餘

(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)資料正規化化的優點

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

比如我們現在新增了一個人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)
})
複製程式碼

相關文章