文档手册

Introduction

2024-07-18 14:52:38

Introduction

该模块提供内置的数据持久性 - 数据存储和检索。

这里的“内置”意味着语言中没有像数据库访问客户端或数据库驱动程序这样的特殊实体来访问持久性数据——而是存储(数据库文件)中的对象是普通的脚本实体:对象(键/值映射)、数组(值列表)和基元类型(字符串、整数、长(又名  BigInt)、float、date、字节向量 (ArrayBuffer)、 boolean 和 null)。

你可以把QuickJS+persistence看作是一个内置NoSQL数据库机制的脚本,其中对数据库中数据的访问是通过标准语言手段提供的:获取/设置存储对象的属性或获取/设置存储数组和索引的元素。

为了支持持久性,本模块引入了两个帮助程序类:Storage 和 Index。

Storage

存储对象表示脚本中的数据库文件。它用于创建数据库,以及打开现有数据库以访问存储在其中的数据。

要创建或打开现有数据库,您将使用 Storage.open(path) 方法。成功后,此方法返回具有很少属性和方法的 Storage 类实例,其中最有趣的是 storage.root 属性:

storage.root

storage.root property 是对存储的根对象(或数组)的引用。

注意

storage.root 对象访问(包含在)对象中的所有对象都是自动持久的 - 存储在数据库中。

就这么简单。 storage.root 是普通的脚本对象,可以通过标准脚本方式用于访问和/或修改存储中的数据。

当您对该根对象下的任何对象或集合进行任何更改时,它将存储到数据库(也称为持久化),而无需将该数据发送到任何地方或显式调用任何方法。

Example #1, storage opening and its structure initialization:

用于打开或创建数据库的惯用代码如下所示:

import * as Storage from "storage"; // or "@storage" if Sciter.JS

var storage = Storage.open("path/to/data/file.db");
var root = storage.root || initDb(storage); // get root data object or initialize DB

where initDb(storage) 仅在刚刚创建存储时调用,因此为空 - 在本例中其根为 null。该函数可能如下所示:

function initDb(storage) {
 storage.root = {
version: 1, // integer property ("integer field" in DB terms)
meta: {}, // sub-object
children: [] // sub-array, empty initially
};
return storage.root;
}

Example #2, accessing and populating persistent data:

有了包含持久根对象的 root 变量,我们可以像往常一样访问和填充其中的数据——不需要其他特殊机制:

// printout elements of root.children collection (array)
let root = storage.root;
for( let child of root.children )
console.log(child.name, child.nickname);

同样,为了填充存储中的数据,我们使用标准 JavaScript 意味着:

var collection = root.children; // plain JS array
 collection.push( { name: "Mikky", age: 7 } ); // calling Array's method push() to add
 collection.push( { name: "Olly", age: 6 } ); // objects to the collection
 collection.push( { name: "Linus", age: 5 } );

正如你所看到的,没有什么特别的——代码与普通的脚本代码没有任何区别,它访问和填充脚本堆中的任何数据。

Index

默认情况下,JavaScript 支持以下内置类型的集合:

  • 对象 – 无序的名称/值映射和

  • 数组 – 按索引(某个整数)访问的值的有序列表。

  • 辅助集合 - [Weak]Map 和 [Weak]Set。

这些集合允许以各种方式组织数据,但有时这些还不够。我们可能需要介于它们之间的一些东西——a) 有序但 b) 允许同时通过键访问元素的集合。示例:我们可能需要按创建时间排序的对象集合,以便我们可以按数据“新鲜度”顺序将集合呈现给用户。

为了支持此类用例,该模块引入了 Index 对象。

提示

索引是一个键控持久性集合,可以分配给其他持久性对象的属性或放入数组中。索引提供对潜在大型数据集的有效访问和排序。

索引支持字符串、整数、长 (BigInt)、浮点键和日期键,并包含对象作为索引元素(也称为记录)。

Defining and populating indexes

索引是按 storage.createIndex(type[,unique]) : Index 方法创建的,其中

  • type 定义索引中的键的类型。它可以是“字符串”、“整数”、“长”、“浮点”或“日期”。

  •  唯一是

    • 如果索引仅支持唯一键,则为 true,或者

    • 如果索引中允许具有相同键值的记录,则为 false。

Example #3: creating indexes, simple storage of notes:

要打开存储数据库,我们可以重用上面的代码,但这次存储初始化例程看起来会有所不同:

function initNotesDb(storage) { 
 storage.root = {
version: 1,
notesByDate: storage.createIndex("date",false), // list of notes indexed by date of creation
notesById:   storage.createIndex("string",true) // list of notes indexed by their UID
}
return storage.root;
}

如您所见,存储包含两个索引:一个将按创建日期列出笔记,另一个将包含相同的笔记,但按唯一 ID 排序。

有了这样的设置,向数据库添加注释是微不足道的:

function addNote(storage, noteText) {
var note = {
id : UUID.create(), // generate UID  
date : new Date(),
text : noteText  
};
 storage.root.notesByDate.set(note.date, note); // adding the note
 storage.root.notesById.set(note.id, note); // to indexes
return note; // returns constructed note object to the caller
}

我们使用 here index.set(key,value) 方法添加项目。

Index selection – traversal and retrieval of index items

获取唯一索引的元素是微不足道的——我们使用 index.get(key) 的方法类似于标准 Map 或 Set 集合的方法:

function getNoteById(noteId) {
return storage.root.notesById.get(noteId); // returns the note or undefined
}

要从非唯一索引中获取项,我们需要一对键来使用 index.select() 方法获取键之间范围内的项:

function getTodayNotes() {
let now = new Date();
let yesterday = new Date(now.year,now.month,now.day-1);
var notes = [];
for(let note of storage.root.select(yesterday,now)) // get range of notes from the index
   notes.push(note);
return notes;
}

Persistence of objects of custom classes

到目前为止,我们处理的是普通对象和数组,但 Storage 也允许存储自定义类的对象。如果数据对象具有特定方法,则此方法非常有用。让我们重构我们的笔记存储,以 OOP 方式使用它:

// module NotesDB.js 

import * as Storage from "storage"; // or "@storage" if Sciter.JS

const storage = ... open DB and optionally initialize the DB ...

class Note {

constructor(text, date = undefined, id = undefined) {
this.id = id || UUID.create();
this.date = date || new Date();
this.text = text;

// adding it to storage
let root = storage.root;
   root.notesByDate.set(this.date, this);
   root.notesById.set(this.id, this);
}

remove() {
let root = storage.root;
   root.notesByDate.delete(this.date, this); // need 'this' here as index is not unique
   root.notesById.delete(this.id);
}

static getById(id) {
return storage.root.notesById.get(id); // will fetch object from DB and do
// Object.setPrototypeOf(note,Note.prototype)
}

}

技术细节:在存储自定义类的对象时,存储将仅存储对象类的名称。数据库既不能包含类本身,也不能包含任何函数——只能包含纯数据。在加载自定义类的对象时,运行时将尝试通过更新此类对象的原型字段来将当前作用域中的类与对象实例绑定。

As an afterword

Sciter Notes 应用程序是存储使用的一个实际示例。

该特定应用程序使用 Sciter/TIScript,但原理相同。您可以在 GitHub 上看到它的数据库处理例程,特别是它的数据库初始化在 JS 中可能看起来像:

//|
//| open database and initialize it if needed
//|

function openDatabase(pathname)
{
//const DBNAME = "sciter-notes.db";
//const pathname = dbPathFromArgs() || ...;
var ndb = Storage.open(pathname);
if(!ndb.root) {
// new db, initialize structure
   ndb.root =
{
id2item :ndb.createIndex("string", true), // main index, item.id -> item, unique
date2item :ndb.createIndex("date", false), // item by date of creation index, item.cdate -> item, not unique
tags :{}, // map of tagid -> tag
books :{}, // map of bookid -> book;
version :1,
};
}
 ndb.path = pathname;
return ndb;
}