手把手教你实现json嵌套对象的范式化和反范式化

手把手教你实现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)
})

猜你喜欢

转载自blog.csdn.net/liwusen/article/details/80657901
今日推荐