原文在我的部落格中:原文地址 如果文章對您有幫助,您的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)

})