手把手教你实现json嵌套对象的范式化和反范式化
在json对象嵌套比较复杂的情况下,可以将复杂的嵌套对象转化成范式化的数据。比如后端返回的json对象比较复杂,前端需要从复杂的json对象中提取数据然后呈现在页面上,复杂的json嵌套,使得前端展示的逻辑比较混乱。
特别的,如果我们使用了flux或者redux等作为我们前端的状态管理机(state对象),通过控制state对象的变化,从而呈现不同的视图层的展示,如果我们在状态管理的时候,将state对象范式化,可以减小state对象操作的复杂性,从而可以清晰的展示视图更新的过程。
- 什么是数据范式化和反范式化
- 数据范式化的实现
- jest编写简单的单元测试
本文的源码地址为:https://github.com/forthealllight/normalize
本文原文在我的github主页中,如果喜欢,您的star是对我最好的鼓励~
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)
})