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