Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
forthealllight opened this issue Jun 7, 2018 · 0 comments
Open

Comments

@forthealllight
Copy link
Owner

forthealllight commented Jun 7, 2018

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


在json对象嵌套比较复杂的情况下,可以将复杂的嵌套对象转化成范式化的数据。比如后端返回的json对象比较复杂,前端需要从复杂的json对象中提取数据然后呈现在页面上,复杂的json嵌套,使得前端展示的逻辑比较混乱。

特别的,如果我们使用了flux或者redux等作为我们前端的状态管理机(state对象),通过控制state对象的变化,从而呈现不同的视图层的展示,如果我们在状态管理的时候,将state对象范式化,可以减小state对象操作的复杂性,从而可以清晰的展示视图更新的过程。

  • 什么是数据范式化和反范式化
  • 数据范式化的实现
  • jest编写简单的单元测试

本文的源码地址为:https://github.com/forthealllight/normalize


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)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant