npm

Share private packages across your team with npm Orgs, now with simplified billing via the aws marketplace!Learn more »

unique-model

0.5.0-alpha.1 • Public • Published

UniqueModel.js

UniqueModel.Js是一个数据模型的抽象层。

注意

现在0.3.x版本中不包含MongoDB的backend,需要使用MongoDB backend的请安装unique-model-mongodb

特性

  • 使用统一的前端接口操作数据模型。
  • 使用可扩展的后端来支持多种数据库(目前只支持MongoDB)。
  • 所有I/O操作都使用async/await封装,因此使用者可以使用async/await,也可以使用Promise

背景

使用Node.js时,常常遇到两个问题。

  • 如果使用Node.js处理复杂的逻辑问题,使用异步代码难以写出易于维护的代码(即使使用了Promise)。而且许多初学者都无法理解异步处理。同时个人感觉同步代码会更易与理解与维护。
  • 如果在项目中使用了两种数据库,或是改变了项目中使用的数据库,我们必须重新实现所有的数据操作,因为这些数据库接口都不同(尤其是非关系型数据库和关系型数据库之间)。
  • 如果一个数据关联了另一个数据(使用唯一ID关联),我们必须根据ID手动查询另一个对象,这是非常麻烦的。

因此,我打算编写一个模块,用来解决这些问题。

第一个版本的UniqueModel是在ES async/await尚未成熟的时候开发的,因此使用了Fibers,但是在使用过程中发现Fibers有各种隐含的性能问题,而且难以部署,因此最后决定使用新标准的async/await重写UniqueModel,并且在API层面上做了合理调整。因此0.1.x版本将无法兼容0.0.x版本。

安装

首先安装 node.js. 因为我使用了ES6中的部分特性(还懒得使用转换器),因此希望大家使用最新的Node.js稳定版本。 接着输入以下命令:

$ npm install unique-model

概述

创建会话

使用UniqueModel的第一步自然是连接到数据库。UniqueModel负责管理数据库连接的部分叫做会话(也就是数据库会话)。 这里需要和Express里session中间件会话进行区分,session中间件的会话是一个HTTP会话,UniqueModel的会话是一个数据库会话。

创建一个会话很简单,比如创建一个MongoDB的会话,只要这样就可以:

const um = require('unique-model');
const umMongo = require('unique-model-mongodb');
umMongo.load(um);

const MongoSession = umMongo.Session;

const databaseSession = await um.createSession({
    backend: MongoSession,
    uri: 'mongodb://user:pwd@host:port/db',
});

createSession的参数是一个对象,包含三个属性:

  • backend: 后端名称。目前只支持mongodb。
  • uri: 需要连接的数据库的URI,URI格式由会话后端决定。
  • options: 会话后端参数。会话后端需要根据选项进行初始化,不同后端会有不同选项。

对于MongoDB的session,uri中的内容要按以下规则替换:

  • user: 数据库用户
  • pwd: 数据库密码
  • host: 服务器主机名
  • port: 端口号
  • db: 数据库名

如果数据库没有开启认证,则需要去掉user:pwd@,变成mongodb://host:port/db

创建持久化会话之后,UniqueModel将会创建会话后端并连接到数据库。

注意: createSession为异步方法,返回的是Promise,所以需要await。

定义模型

下一步就是定义模型。统一模型将会创建模型类,并记录其字段信息。

const model = um.model;
const types = um.type;

const UInteger = types.UInteger;
const UDouble = types.UDouble;
const UBoolean = types.UBoolean;
const UString = types.UString;
const UObject = types.UObject;
const UObjectArray = types.UObjectArray;
const UDateTime = types.UDateTime;
const UMixed = types.UMixed;

const User = model.createModel('User', {
    loginName: UString(),
    password: UString(),
    role: UObject({
        type: 'Role'
    })
}, 'users');

const Role = model.createModel('Role', {
    name: UString(),
    level: UInteger(),
    privileges: UObjectArray({
        type: 'Privilege'
    })
}, 'roles');

const Privilege = model.createModel('Privilege', {
    name: UString(),
    status: UInteger(),
    userData: UMixed()
}, 'privileges');

第一个参数是模型名。

第二个参数是字段信息。UniqueModel使用um.type中的函数来定义类型。 没种类型都有一些选项,用户需要在函数参数中指定这些选项。

目前我们支持以下几种类型:

  • UInteger: 整数字段。
  • UDouble: 数字字段。
  • UBoolean:布尔字段
  • UString: 文本字段。
  • UObject: 对象字段,关联另一个模型对象(1对1或者多对1)
  • UObjectArray: 对象数组,关联另一组对象,其类型相同(1对多或多对多)
  • UDateTime: 日期字段
  • UMixed: 任意类型(非特殊情况慎用,没有模式检查)

UObject和UObjectArray都有一个名为type的选项。该选项用于指定关联模型的名字。

创建模型实例

接着我们使用new来创建一个模型实例。 你可以创建一个空对象,并逐个指定属性,如下所示:

let user = new User();
user.loginName = 'kkk';
user.password = 28;

也可以使用JavaScript对象指定字段的初始值。 初始化支持递归创建对象(会自动创建子模型实例)。

let user = new User({
    loginName: 'abc',
    password: 'abcp', 
    role: {
        name: 'admin',
        level: 1,
        privileges: [ {
            name: 'view',
            status: 1,
        }, {
            name: 'modify',
            status: 1,
            userData: {
                db: {
                    name: 'imdb',
                    collection: {
                        name: 'people',
                        auth: [ 'delete', 'get', 'post' ]
                    }
                }
            }
        } ]
    }
});

获取DAO(Data Access Object)

现在我们就可以将模型实例持久化到数据库中。 但由于我将持久化操作分离到了DAO对象中,每个模型都会有一个DAO对象。 我们可以使用会话对象的getDao方法来获取DAO。

const userDao = databaseSession.getDao(User);
const roleDao = databaseSession.getDao(Role);
const privilegeDao = databaseSession.getDao(Privilege);

每个DAO对象都会和一个会话关联。因此你可以在一个项目中使用不同的会话后端。

持久化模型实例到数据库中

我们现在就可以持久化模型实例了。

create方法

使用DAO对象的create方法可以持久化模型实例:

console.log(user);
await userDao.create(user);
console.log(user);

这里需要注意每个对象在持久化后会有一个唯一id,这个id会与数据中的记录关联。因此持久化之后再输出user对象,会多出一个id属性。

此外,涉及到与持久化层(数据库)交互的时候记得使用await来实现同步调用,否则create或返回一个Promise,后面所有的数据库操作都是同理。

insertMany方法

如果想要插入大批量数据直接使用create逐个插入会很慢,因此建议使用insertMany方法将大量数据一起插入到数据库中。

await userDao.insertMany([{
    loginName: 'user1',
    role: role1.id
}, {
    loginName: 'user1',
    role: role1.id
}]);

这里需要注意的是insertMany接收的是普通JavaScript对象的数组,而不是UniqueModel对象。此外原本关联其他类型对象的属性需要直接写对象的id(因为数据库内部就是这样储存的)。

这样将会直接在数据库中插入原始数据,而且不会进行任何模式检查,所以性能比逐个create对象好很多。

查找模型

接着让我们查找持久化在数据库中的用户。 DAO的find方法用于查找所有符合条件的模型实例。 查询条件会直接传递给后端。 因此如果你使用mongodb后端,其查询格式和MongoDB相同。

find方法

find的返回值是一个对象数组。可以使用forEach方法遍历该数组。 如果数据库中找不到匹配对象则返回空数组。

let users = await UserDao.find({
    loginName: 'kinuxroot'
});

users.forEach(user => {
    console.log(user);
});

如果没有查询参数,则是返回所有对象。

let users = await UserDao.find();

users.forEach(user => {
    console.log(user);
});

注意: find方法不支持skip/limit/sort等高级操作

findOne方法

你也可以使用findOne方法获取某个满足查询条件的对象。

let user = await UserDao.findOne({
    loginName: 'ttt'
});

console.log(user);

如果数据库中找不到匹配对象则返回null。

findOne还支持第二个参数,该参数可以指定按某个字段排序,并且可以选择排序后的第n个数字。比如如果你希望按照password增序排序,并且选择结果中的第3个对象,那么可以这样写代码:

let user = await UserDao.findOne({
    loginName: 'ttt'
}, {
    sort: {
        password: 1
    },
    skip: 2
});

console.log(user);
  • sort表示排序用的字段,1表示增序,-1表示降序
  • skip表示跳过几个对象,skip: 2就是跳过前2个对象,选择第3个对象

query方法

常用的find方法其实只是query方法的一个语法糖。如果想要支持更完善的查询功能,需要使用query方法。

query方法的完整用法如下所示:

let users = await 
    UserDao.query({
        loginName: 'kinuxroot'
    })
    .sort({
        password: 1
    })
    .skip(3)
    .limit(5)
    .execute();

users.forEach(user => {
    console.log(user);
});

其中query方法的参数和find方法一致,唯一区别的是find方法直接返回了查询的Promise,而query方法则返回的是一个Query对象,我们可以在这个Query对象上加上更多的限制:

  • sort: 指定排序规则,同findOne中的sort
  • skip: 指定跳过的对象数量,同findOne中的skip
  • limit: 指定返回的对象数量

比如示例中就是查询loginName为kinuxroot的所有对象,且按照password增序排序,跳过3个对象,返回至多5个对象。

记住这里所有的操作返回的都是Query对象,最后想要获取结果需要调用Query对象的execute方法,将会返回包含执行查询动作的Promise。只有这个操作是需要await的。

如果没有查询到满足条件的对象,则会返回空数组。

从实现来说,find只是query的一个语法糖,差不多实现如下:

async find(options) {
    return await query(options).execute();
}

更新模型

updateOne

如果想要更新某个模型实例,可以使用updateOne方法。

现在让我们更新用户信息。

const user = await userDao.findOne({
    loginName: 'dddd'
});

user.loginName = 'djfk';
user.password = 'kkav';
await userDao.updateOne(user);

console.log(user);

调用updateOne后,UniqueModel将会使用新数据替换掉数据库中的原有模型数据。

update

你也可以根据条件更新某个模型实例。

比如:

await userDao.update({
    loginName: 'abd'
}, {
    $set: {
        loginName: 'dddd'
    }
}, {
    multi: true
});

意思就是更新loginName为abd的对象,将loginName改为dddd,第三个选项中的multi:true表示支持同时匹配与更新多个对象实例,否则只会匹配并修改满足条件的第一个对象。

findOneAndUpdate

update的过程不确保原子性的(根据后端实现),因此如果想要原子性地查找并修改一个属性,需要使用findOneAndUpdate方法。该方法用法和update一样:

await userDao.findOneAndUpdate({
    loginName: 'abd'
}, {
    $set: {
        loginName: 'dddd'
    }
}, {
    multi: true
});

删除模型

删除某个对象

从数据库中删除模型也是非常简单的,直接在UniqueModel对象上执行remove即可。

const user = await userDao.findOne({
    loginName: 'djfk'
});

await userDao.remove(user);

调用remove之后,模型数据就会从数据库中删除了。

根据条件删除对象

DAO还支持根据条件删除对象,示例如下。

await userDao.remove({
    loginName: 'djfk'
});

只不过这样将会删除所有满足条件的对象。

如果大括号中查询条件为空则表示删除所有对象。

await userDao.remove({});

统计功能

count

当想要统计满足条件的记录数量的时候我们可以使用find然后计算返回数组的长度,但是这样效率太低,所以UniqueModel还支持count方法,用来计算满足条件的对象数量,但是不返回对象本身。

比如我们可以这样计算password为123456的用户数量。

const count = await userDao.count({
    password: '123456'
});
console.log(count);

如果没有查询条件则是返回所有对象数量。

const count = await userDao.count();
console.log(count);

操作关联对象属性

新版本UniqueModel中一个最大的改动就是抛弃了Fibers,因此所有可能隐含I/O操作的地方都会返回一个Promise。所以第一个版本中的惰性加载机制有了很大变化。

比如当你查询出一个对象的时候

let user = await userDao.findOne({
    loginName: 'kinuxroot'
});

会查询出该对象的所有属性。但是user中包含的对象属性,比如role是不会被查询出来的,因为role可能存储在数据库的其他表或者集合中,因此该对象会有一个需要加载的状态。这时如果访问role的属性就会抛出一个异常。

console.log(user.role.name);

因此你需要使用DAO的load方法将该对象从数据库中加载出来,如下所示。

await roleDao.load(user.role);
console.log(user.role.name);

如果关联的是数组,则需要逐个加载。或者直接对数组使用load。

for ( const privilege of user.role.privileges ) {
    await privilegeDao.load(privilege);
}

或者

await privilegeDao.load(user.role.privileges);

如果你希望有自动惰性加载,那么可以使用$value或者$extract,这个将会在其他功能中介绍。

其他功能

为了方便使用者,UniqueModel还提供了更多的工具方法。

$extract

$extract的功能是从某个对象中按特定格式提取出相应的数据,并组织成完整的对象层次关系。这个在查询后向客户端返回数据的时候会常常用到。

$extract的调用方法如下。

let result = await umObject.$extract(options);

由于extract的过程可能发生数据库查询,因此需要使用await来调用。

options有两个选项可供选择:

  • recursive: 默认是false,如果是true则extract会递归提取出该对象关联的所有数据(包括关联的子对象)
  • includes: 指定字段,只会将特定的字段提取出来。

includes的形式如下:

includes: {
    loginName: true,
    password: true,
    role: {
        name: true,
        privileges: true
    }
}
  • 每个字段表示需要提取的字段,true表示需要该字段
  • 如果字段的类型是对象或者对象数组,那么只会提取其id
  • 如果想要提取子对象中的字段,则需要在子对象字段后面写一个对象,对象里面包含需要提取的字段

$extractArray(静态方法)

如果想要提取UniqueModel对象数组中的数据,则使用$extractArray,$extractArray为模型类的静态方法:

let result = await User.$extractArray(users, {
    includes: {
        loginName: true,
        password: true,
        role: {
            name: true,
            privileges: true
        }
    }
});

参数和$extract参数相同,只不过返回的是对象数组。其中users是需要提取的对象数组。

$value

如果不想按对象结构提取数据,只想提取UniqueModel对象中特定路径对应的字段,可以使用$value。

$value用法如下所示:

const privilegeStatus = await user.$value('role.privileges[0].status');
console.log(privilegeStatus);

其实就是把原来正常访问值的表达式变成字符串传递给$value。由于过程中可能需要查询数据库,因此该方法返回的也是Promise,建议使用await处理。

更多

DAO模型有更多的更新和删除方法,你可以阅读源代码目录中的test/test.js获知这些方法的用法。

更新日志

  • 2015/10/26: 创建项目。
  • 2015/10/28: 支持统一模型(无持久化)。
  • 2015/11/02: 实现Mongoose的持久化后端。
  • 2015/11/02: 使用持久话后端实现简单模型的基本CRUD功能。
  • 2015/11/05: 支持惰性加载和自动查询。
  • 2015/11/20: 支持对象数组。Support Array of Object.
  • 2015/11/23: 调整UObject和UObjectArray的fromValue函数。
  • 2015/11/24: 支持直接更新模型。
  • 2015/11/25: 完成基本的README。
  • 2015/11/25: 完成中文版说明。
  • 2015/11/27: 支持toObject。
  • 2015/11/27: 支持Boolean和Date类型。
  • 2015/12/01: 支持Model.toObjectArray工具方法。
  • 2015/12/01: 修复当对象类型属性为null时转成普通对象会出现异常的bug。
  • 2015/12/01: 修复递归解析对象bug。
  • 2015/12/05: 修复toObject方法中特定字段的解析bug。
  • 2015/12/05: 会自动替换查询条件中的id字段。
  • 2015/12/07: 支持在toObjectArray中加入和toObject一样的选项。
  • 2106/08/20: 支持查询中加入$offset/$limit/$sort。
  • 2106/11/11: 支持distinct、aggregate、count。
  • 2017/01/14: 支持insertMany。
  • 2017/01/14: 修复updateOne的bug。
  • 2017/02/13: 修复updateOne无法查找到任何对象会抛出异常的问题。
  • 2017/05/22: 完成新版本UniqueModel。

Keywords

install

npm i unique-model

Downloadsweekly downloads

73

version

0.5.0-alpha.1

license

MIT

repository

Gitgit

last publish

collaborators

  • avatar
Report a vulnerability