NeDB:轻量级JavaScript数据库

NeDB: A Lightweight JavaScript Database

当您想到数据库时,首先想到的可能是MySQL,MongoDB或PostgreSQL。 尽管这些都是存储数据的不错选择,但对于大多数应用程序来说,它们都太强大了。

考虑用JavaScript用Electron框架编写的桌面聊天应用程序。 尽管聊天数据(消息,联系人,历史记录等)可能源自API服务器,但也需要将其存储在应用程序本地。 您可能有成千上万条消息,所有这些消息都需要存储以方便访问和搜索。

所以你会怎么做? 一种选择是将所有这些数据存储在某个位置的文件中,并在每次需要检索时都进行搜索,但这可能效率不高。 另一个选择是不每次需要更多数据时都不在本地缓存数据并调用API服务器,但是这样您的应用程序将响应速度变慢,并且将消耗更多的网络数据。

更好的主意是使用嵌入式/轻量级数据库,例如NeDB。 这更有意义,因为您的应用程序将无法为成千上万的用户提供服务或处理千兆字节的数据。

NeDB与SQLite非常相似,它是更大数据库系统的更小,可嵌入的版本。 NeDB不是模仿较小的SQL数据存储,而是模仿MongoDB的较小的NoSQL数据存储。

轻量级数据库通常将其数据存储在内存中或纯文本文件中(带有用于快速查找的索引)。 这有助于减少系统上数据库的总体占用空间,这对于较小的应用程序而言是完美的选择。 为了进行比较,MySQL tar文件(对于Mac OSX)为337MB,而NeDB(未压缩,未压缩)仅为1.5MB。

特别是有关NeDB的最重要的事情之一就是它的API是MongoDB API的子集,因此,如果您熟悉MongoDB,那么在初始设置后使用NeDB就不会有问题。

注意:从v1.8.0开始,NeDB尚未更新为Mongo的某些新方法名称,例如insertOneinsertManyfindOne的删除。

NeDB入门

首先,使用NPM安装模块:

1
$ npm install nedb --save

该模块是用纯JavaScript编写的,因此编译本机加载项不会出现任何问题,就像MongoDB驱动程序有时会存在问题一样。

如果您打算在浏览器中使用它,请使用Bower安装:

1
$ bower install nedb

像所有数据库客户端一样,第一步是连接到后端数据库。 但是,在这种情况下,没有外部应用程序可以连接,因此我们只需要告诉它您数据的位置即可。 使用NeDB,您可以选择几种方法来保存数据。 第一种选择是将数据保存在内存中:

1
2
3
4
var Datastore = require('nedb');
var db = new Datastore();

// Start issuing commands right away...

这将使您开始时没有任何数据,并且退出应用程序时,所有保存的数据都将丢失。 尽管它非常适合在测试或较短的会话期间使用(例如在浏览器中)。

或者另一个选择是将数据保存到文件中。 此处的区别在于,您需要指定文件位置并加载数据。

1
2
3
4
5
6
var Datastore = require('nedb');
var db = new Datastore({ filename: 'path/to/your/file' });

db.loadDatabase(function(err) {
    // Start issuing commands after callback...
});

如果不想为加载的每个数据库调用db.loadDatabase,则也可以始终使用autoload: true选项。

需要注意的重要一件事是,每个文件都相当于MongoDB中的一个集合。 因此,如果您有多个集合,则需要在启动时加载多个文件。 因此您的代码可能如下所示:

1
2
3
4
var Datastore = require('nedb');
var users = new Datastore({ filename: 'users.db', autoload: true });
var tweets = new Datastore({ filename: 'tweets.db', autoload: true });
var messages = new Datastore({ filename: 'messages.db', autoload: true });

保存数据

从文件加载数据(或创建内存存储)后,您将要开始保存数据。

与Mongo驱动程序非常相似,您将使用insert创建一个新文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Datastore = require('nedb');
var users = new Datastore();

var scott = {
    name: 'Scott',
    twitter: '@ScottWRobinson'
};

users.insert(scott, function(err, doc) {
    console.log('Inserted', doc.name, 'with ID', doc._id);
});

// Prints to console...
// (Note that ID will likely be different each time)
//
//"Inserted Scott with ID wt3Nb47axiOpme9u"

此插入可以轻松扩展以一次保存多个文档。 使用相同的方法,只需传递一个对象数组,每个对象将被保存并在回调中返回给您:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var Datastore = require('nedb');
var users = new Datastore();

var people = [];

var scott = {
    name: 'Scott Robinson',
    age: 28,
    twitter: '@ScottWRobinson'
};

var elon = {
    name: 'Elon Musk',
    age: 44,
    twitter: '@elonmusk'
};

var jack = {
    name: 'Jack Dorsey',
    age: 39,
    twitter: '@jack'
};

people.push(scott, elon, jack);

users.insert(people, function(err, docs) {
    docs.forEach(function(d) {
        console.log('Saved user:', d.name);
    });
});

// Prints to console...
//
// Saved user: Scott Robinson
// Saved user: Elon Musk
// Saved user: Jack Dorsey

更新现有文档的工作原理几乎相同,除了需要提供查询以告知系统需要更新哪些文档之外。

加载数据中

现在我们已经保存了一堆数据,是时候从数据库取回数据了。 同样,我们将使用find方法遵循与Mongo相同的约定:

1
2
3
4
5
6
7
8
9
10
11
12
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.findOne({ twitter: '@ScottWRobinson' }, function(err, doc) {
    console.log('Found user:', doc.name);
});

// Prints to console...
//
// Found user: Scott Robinson

同样,我们可以使用类似的操作来检索多个文档。 返回的数据只是匹配文档的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Datastore = require('nedb');
var users = new Datastore();

// Save a bunch of user data here...

users.find({ age: { $lt: 40 }}, function(err, docs) {
    docs.forEach(function(d) {
        console.log('Found user:', d.name);
    });
});

// Prints to console...
//
// Found user: Jack Dorsey
// Found user: Scott Robinson

您可能已经从最后一个代码示例中注意到,NeDB如您所料,能够处理更复杂的查询,例如数字比较。 以下运算符均可用于查找/匹配文档:

  • $lt$lte:小于,小于或等于

  • $gt$gte:大于,大于或等于

  • $in:数组中包含的值

  • $nin:值未包含在数组中

  • $ne:不等于

  • $exists:检查给定属性的存在(或不存在)

  • $regex:用正则表达式匹配属性的字符串

  • 您还可以使用标准的排序,限制和跳过操作。 如果没有为find方法提供回调,则将改为返回Cursor对象,您可以将其用于排序,限制和跳过。 这是一个按名称按字母顺序排序的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var Datastore = require('nedb');
    var users = new Datastore();

    // Save a bunch of user data here...

    users.find({}).sort({name: 1}).exec(function(err, docs) {
        docs.forEach(function(d) {
            console.log('Found user:', d.name);
        });
    });

    // Prints to console...
    //
    // Found user: Elon Musk
    // Found user: Jack Dorsey
    // Found user: Scott Robinson

    其他两个操作(跳过和限制)的工作与此非常相似。

    findfindOne方法还支持许多其他运算符,但是我们在这里不做所有介绍。 您可以在自述文件的查找文档部分中详细了解这些操作的其余部分。

    删除资料

    关于删除数据,除了与find方法类似的工作外,没有什么要说的。 您将使用相同类型的查询在数据库中查找相关文档。 找到的内容将被删除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var Datastore = require('nedb');
    var users = new Datastore();

    // Save a bunch of user data here...

    users.remove({ name: { $regex: /^Scott/ } }, function(err, numDeleted) {
         console.log('Deleted', numDeleted, 'user(s)');
    });

    // Prints to console...
    //
    // Deleted 1 user(s)

    默认情况下,remove方法仅删除单个文档。 为了通过一次调用删除多个文档,必须将multi选项设置为true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var Datastore = require('nedb');
    var users = new Datastore();

    // Save a bunch of user data here...

    users.remove({}, { multi: true }, function(err, numDeleted) {
         console.log('Deleted', numDeleted, 'user(s)');
    });

    // Prints to console...
    //
    // Deleted 3 user(s)

    资料索引

    就像任何其他数据库一样,您可以在数据上设置索引以加快检索速度或强制执行某些约束(例如唯一值)。 若要创建索引,请使用ensureIndex方法。

    当前支持的三种索引类型是:

  • unique:确保给定字段在整个集合中是唯一的

  • sparse:不要索引未定义给定字段的文档

  • expireAfterSeconds:在给定的秒数(有效时间或TTL)后删除文档

  • 在我看来,TTL索引特别有用,因为它使您不必编写代码即可频繁扫描和删除已过期的数据。

    例如,这对于密码重置请求很有用。 如果您的数据库中存储了PasswordReset对象,则您不希望该对象永远有效。 为了保护用户,它可能应该过期并在几天后将其删除。 此TTL索引可以为您处理删除它。

    在以下示例中,我们将unique约束放置在文档的Twitter句柄上。 这意味着,如果一个用户使用与另一个用户相同的Twitter句柄进行保存,将引发错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var Datastore = require('nedb');
    var users = new Datastore();

    users.ensureIndex({ fieldName: 'twitter', unique: true });

    var people = [];

    var jack = {
        name: 'Jack Dorsey',
        age: 39,
        twitter: '@jack'
    };

    var jackSmith = {
        name: 'Jack Smith',
        age: 68,
        twitter: '@jack'
    };

    people.push(jack, jackSmith);

    users.insert(people, function(err, docs) {
        console.log('Uh oh...', err);
    });

    // Prints to console...
    //
    // Uh oh... Can't insert key @jack, it violates the unique constraint

    更进一步

    尽管NeDB API易于使用且应有尽有,但如果没有深思熟虑和井井有条,您的代码就会变得很难使用。 这是对象文档映射器(就像ORM)进入的地方。

    使用Camo ODM(我创建的),您可以简单地将NeDB数据存储视为JavaScript类。 这使您可以指定模式,验证数据,扩展模式等。 Camo甚至还可以与MongoDB一起使用,因此您可以在测试/开发环境中使用NeDB,然后将Mongo用于您的生产系统,而无需更改任何代码。

    这是一个连接数据库,声明一个类对象并保存一些数据的快速示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    var connect = require('camo').connect;
    var Document = require('camo').Document;

    class User extends Document {
        constructor() {
            super();

            this.name = String;
            this.age = Number;
            this.twitter = Sring;
        }

        get firstName() {
            return this.name.split(' ')[0];
        }
    }

    var scott = User.create({
        name: 'Scott Robinson',
        age: 28,
        twitter: '@ScottWRobinson'
    });

    var elon = User.create({
        name: 'Elon Musk',
        age: 44,
        twitter: '@elonmusk'
    });

    connect('nedb://memory').then(function(db) {
        return Promise.all([scott.save(), elon.save()]);
    }).then(function(users) {
        users.forEach(function(u) {
            console.log('Saved user:', u.firstName);
        });

        return elon.delete();
    }).then(function() {
        console.log('Deleted Elon!')
    });

    // Prints to console...
    //
    // Saved user: Scott
    // Saved user: Elon
    // Deleted Elon!

    除了我在这里展示的内容之外,此ODM还有很多其他内容。 有关更多信息,请查看本文或项目的自述文件。

    结论

    由于NeDB非常小(而且非常快!),因此很容易将其添加到几乎任何项目中。 而且,在混合使用Camo的情况下,只需几行代码即可声明基于类的对象,这些对象更易于创建,删除和操作。

    如果您曾经在一个项目中使用过NeDB,我们很乐意听到有关它的信息。 让我们在评论中知道!