/ReactiveDB

Reactive ORM for Lovefield

Primary LanguageTypeScriptMIT LicenseMIT

CircleCI Coverage Status Dependency Status devDependencies Status Greenkeeper badge

ReactiveDB

一个 Reactive 风格的前端 ORM。基于 LovefieldRxJS

Features

  • 响应式查询

    支持以 Observable 的形式返回响应式数据

  • 数据一致性

    所有的执行过程都是事务性的,在遇到环境异常时(indexDB 异常,浏览器限制,隐私模式导致的功能性缺失等) 也不会产生脏数据。

  • 数据持久化

    大量的数据场景下,极端如单页应用不间断运行几个月的情况下,不会造成内存占用量过多。所有的数据都可以持久化在本地存储而非内存中,并支持丰富的数据换页配置[WIP]。

  • debug tools

    Lovefield debug tool for Chrome

Documents

Scenarios

在单页实时性应用的场景下,抽象出在前端维护数据以及其关联的数据的变更的逻辑

考虑下面的场景,在一个单页前端应用中,需要展示 A,B, C, D 四个列表:

其中列表 A 展示所有 ownerId 为 user1 的 Item :

[
  {
    "_id": 1,
    "name": "item 1",
    "ownerId": "user1",
    "creatorId": "user2",
    "created": "2016-01-31T16:00:00.000Z",
    "owner": {
      "_id": "user1",
      "name": "user1 name"
    },
    "creator": {
      "_id": "user2",
      "name": "user2 name"
    }
  },
  {
    "_id": 3,
    "name": "item 1",
    "ownerId": "user1",
    "creatorId": "user3",
    "created": "2016-05-03T16:00:00.000Z",
    "owner": {
      "_id": "user1",
      "name": "user1 name"
    },
    "creator": {
      "_id": "user3",
      "name": "user3 name"
    }
  }
  ...
]

列表 B 展示所有 creatorId 为 user2 的 Item:

[
  {
    "_id": 1,
    "name": "item 1",
    "ownerId": "user1",
    "creatorId": "user2",
    "created": "2016-01-31T16:00:00.000Z",
    "owner": {
      "_id": "user1",
      "name": "user1 name"
    },
    "creator": {
      "_id": "user2",
      "name": "user2 name"
    }
  },
  {
    "_id": 2,
    "name": "item 1",
    "ownerId": "user2",
    "creatorId": "user3",
    "created": "2016-04-20T16:00:00.000Z",
    "owner": {
      "_id": "user2",
      "name": "user2 name"
    },
    "creator": {
      "_id": "user3",
      "name": "user3 name"
    }
  }
  ...
]

列表 C 展示所有 created 时间为 2016年3月1日 以后的 Item:

[
  {
    "_id": 2,
    "name": "item 1",
    "ownerId": "user2",
    "creatorId": "user3",
    "created": "2016-04-20T16:00:00.000Z",
    "owner": {
      "_id": "user2",
      "name": "user2 name"
    },
    "creator": {
      "_id": "user3",
      "name": "user3 name"
    }
  },
  {
    "_id": 3,
    "name": "item 1",
    "ownerId": "user1",
    "creatorId": "user3",
    "created": "2016-05-03T16:00:00.000Z",
    "owner": {
      "_id": "user1",
      "name": "user1 name"
    },
    "creator": {
      "_id": "user3",
      "name": "user3 name"
    }
  }
]

列表 D 展示所有的用户信息:

[
  {
    "_id": "user1",
    "name": "user1 name",
    "avatarUrl": "user1 avatarUrl",
    "birthday": "user1 birthday"
  },
  {
    "_id": "user2",
    "name": "user2 name",
    "avatarUrl": "user2 avatarUrl",
    "birthday": "user2 birthday"
  },
  {
    "_id": "user3",
    "name": "user3 name",
    "avatarUrl": "user3 avatarUrl",
    "birthday": "user3 birthday"
  }
]

这四个列表的数据分别从四个 API 获取。在大多数单页应用的架构中,数据层会缓存这几个接口的数据,避免重复请求。而在实时性的单页应用中,这些数据的更新通常需要通过 WebSocket 等手段进行更新。根据缓存策略的不同(单例存储/同一 ID 存储多份数据),则有不同的更新方式。但这个过程一般是 业务化且难以抽象 的。

比如单一引用存储数据时, 上面场景中列举到的数据只会存储为:

{
  item1, item2, item3,
  user1, user2, user3
}

在这种缓存策略下,一个数据变更后,将变更后的结果通知到所属的集合是一件非常麻烦的事情。 假设现在我们的应用收到一条 socket 消息:

{
  "change:item1": {
    "ownerId": "user3"
  }
}

按照业务需求我们应该将 item1ListA 中移除。在这种缓存策略中,如果使用的 pub/sub 的模型进行通知(Backbone 之类),则会导致数据层外的代码不得不进行大量的计算,不停的 filter 一个变更是否满足某个列表的需求。这种重复的过程是非常难以维护,业务化,且难以抽象的。 而按照 ReactiveDB 的设计理念,所有的数据都有可选的响应模式,即任何与之相关的变动都会让数据自行更新为最新的值:

伪代码如下:

/**
 * @param tableName
 * @param queryOptions
 * @return QueryToken<T>
 **/
database.get<ItemSchema>('Item', {
  where: {
    ownerId: 'user1'
  },
  fields: [
    '_id', 'name', 'ownerId',
    {
      owner: ['_id', 'name']
    }
  ]
})
  .changes()
  .subscribe(items => {
    console.log(items)
  })

使用 ReactiveDB 的情况下,无论是 Item 本身的变更还是与之关联的 User 变更,都会产生新的 items 值。 更复杂的比如 ListC:

/**
 * @param tableName
 * @param queryOptions
 * @return QueryToken<T>
 **/
database.get<ItemSchema>('Item', {
  where: {
    created: {
      // $gte means great than and equal
      // 更多操作符参见详细的使用文档
      '$gte': new Date(2016, 3, 1).valueOf()
    }
  },
  fields: [
    '_id', 'name', 'ownerId',
    {
      owner: ['_id', 'name']
    }
  ]
})
  .changes()
  .subscribe(items => {
    console.log(items)
  })