概述
LokiJS持久化是通过适配器接口实现的。我们支持自动保存
和自动加载
选项,简单的键/值
适配器以及参考模式
的适配器,并且现在支持各种结构化序列化方法,可以轻松创建自己的持久化适配器以及批量或流式数据交换。
像lokijs
这样的内存数据库和传统数据库系统之间的一个重要区别是,所有文档/记录都保存在内存中,并且不会根据需要加载。因此持久化仅用于保存和恢复
这个内存数据库的状态。
如果您的数据库足够小,并且您希望尝试,则可以调用
db.serialize()
以返回完整序列化的数据库,并将其加载到另一个数据库实例中,如dbcopy.loadJSON(str)
。
Node.js 快速开始
If you are using lokijs in a node environment, we will automatically detect and use the built-in LokiFsAdapter without your needing to provide an adapter.
No Persistence example (entirely synchronous and in memory) :
1 | const loki = require("lokijs"); |
Autosave/autoload quickstart with default LokiFsAdapter (async i/o) :
1 | var db = new loki('quickstart.db', { |
If you expect your database to grow over 100mb or you experience slow save speeds you might to use our more high-performance LokiFsStructuredAdapter. This adapter utilitizes es6 generator iterators and node streams to stream the database line by line. It will also save each collection into its own file (partitioned) with a file name derived from the base name. This database should scale to support databases just under 1 gb on the default node heap allocation of 1.4gb. Increasing heap allocation, you can push this limit further.
An example using fastest and most scalable LokiFsStructuredAdapter (for nodejs) might look like :
1 | const loki = require("lokijs"); |
Web 快速开始
If you are using lokijs in a web environment, we will automatically use the built-in LokiLocalStorageAdapter. This adapter is limited to around 5mb so that won’t last long but here is how to quickly get started experimenting with lokijs :
1 | <script src="../../src/lokijs.js"></script> |
Example constructing loki for in-memory only or manual save/load (with default localStorage adapter) :
1 | var loki = new loki("test.db"); |
Example constructing loki for autoload/autosave (with default localStorage adapter) :
1 | var db = new loki("quickstart.db", { |
If you expect your database to grow up to 60megs you might want to use our LokiIndexedAdapter which can save to IndexedDb, if your browser supports it.
Example using more scalable LokiIndexedAdapter :
1 | <script src="../../src/lokijs.js"></script> |
1 | var idbAdapter = new LokiIndexedAdapter(); |
If you expect your database to grow over 60megs things start to get browser dependent. To provide singular guidance and since Chrome is the most popular web browser you will want to employ our LokiPartitioningAdapter in addition to our LokiIndexedAdapter. To sum up as briefly as possible, this will divide collections into their own files and if a collection exceeds 25megs (customizable) it will subdivide into separate pages(files). This allows our indexed db adapter to accomplish a single database save/load using many key/value pairs. This adapter will allow scaling up to around 300mb or so in current testing.
An example using the LokiPartitioningAdapter along with LokiIndexedAdapter might appear as :
1 | <script src="../../src/lokijs.js"></script> |
1 | var idbAdapter = new LokiIndexedAdapter(); |
Description of LokiNativescriptAdapter
This adapter can be used when developing a nativescript application for iOS or Android, it persists the loki db to the filesystem using the native platform api.
Simple Example of using LokiNativescriptAdapter :
1 | const loki = require ('lokijs'); |
In addition to the above adapters which are included in the lokijs distro, several community members have also created their own adapters using this adapter interface. Some of these include :
- Cordova adapter : https://github.com/cosmith/loki-cordova-fs-adapter
- localForage adapter : https://github.com/paulhovey/loki-localforage-adapter
Configuring persistence adapters
Autosave, Autoload and close()
LokiJS now supports automatic saving at user defined intervals, configured via loki constructor options. This is supported for all persistenceMethods. Data is only saved if changes have occurred since the last save. You can also specify an autoload to immediately load a saved database during new loki construction. If you need to process anything on load completion you can also specify your own autoloadCallback. Finally, in an autosave scenario, if the user wants to exit or is notified of leaving the webpage (window.onbeforeunload) you can call close() on the database which will perform a final save (if needed).
*Note : the ability of loki to ‘flush’ data on events such as a browsers onbeforeunload event, depends on the storage adapter being synchronous. Local storage and file system adapters are synchronous but indexeddb is asynchronous and cannot save when triggered from db.close() in an onbeforeunload event. The mouseleave event may allow enough time to perform a preemptive save.*
Autosave example
1 | var idbAdapter = new LokiIndexedAdapter('loki'); |
Autosave with autoload example
1 | var idbAdapter = new lokiIndexedAdapter('loki'); |
Save throttling and persistence contention management
LokiJS now supports throttled saves and loads to avoid overlapping saveDatabase and loadDatabase calls from interfering with each other. This is controlled by a loki constructor option called ‘throttledSaves’ and the default for that option is ‘true’.
This means that within any single Loki database instance, multiple saves routed to the persistence adapter will be throttled and ensured to not conflict by overlap. With save throttling, during the time between an adapter save and an adapter response to that save, if new save requests come in we will queue those requests (and their callbacks) for a save which we will initiate immediately after the current save is complete. In that situation, if 10 requests to save had been made while a save is pending, the subsequent (single) save will callback all ten queued/tiered callbacks when -it- completes.
If a loadDatabase call occurs while a save is pending, we will (by default) wait indefinitely for the queue to deplete without being replenished. Once that occurs we will lock all saves during the load… any incoming save requests made while the database is being loaded will then be queued for saving once the load is completed. Since loadDatabase now internally calls a new ‘throttledSaveDrain’ we will pass through options to control that drain. (These options will be summarized below).
You may also directly call this ‘throttledSaveDrain’ loki method which can wait for the queue to drain. You might do this using any of these variations/options :
1 | // wait indefinitely (recursively) |
1 | // wait only for the -current- queue to deplete |
1 | // wait recursively but only for so long... |
If you do not wish loki to supervise these conflicts with its throttling contention management, you can disable this by constructing loki with the following option (in addition to any existing options you are passing) :
1 | var db = new loki('test.db', { throttledSaves: false }); |
Creating your own Loki Persistence Adapters
Lokijs currently supports two types of database adapters : ‘basic’, and ‘reference’ mode adapters. Basic adapters are passed a string to save and return a string when loaded… this is well suited to key/value stores. Reference mode adapters are passed a reference to the database itself where it can save however it wishes to. When loading, reference mode adapters can return an object reference or serialized string. Below we will describe the minimal functionality which lokijs requires, you may want to provide additional adapter functionality for deleting or inspecting its persistence store.
Creating your own ‘Basic’ persistence adapter
1 | MyCustomAdapter.prototype.loadDatabase = function(dbname, callback) { |
and a saveDatabase example might look like :
1 | MyCustomAdapter.prototype.saveDatabase = function(dbname, dbstring, callback) { |
Creating your own ‘Reference Mode’ persistence adapter
An additional ‘level’ of adapter support would be for your adapter to support ‘reference’ mode support. This ‘reference’ mode will allow lokijs to provide your adapter with a reference to a lightweight ‘copy’ of the database sharing only the collection.data[] document object instances with the original database. You would use this reference to destructure or save however you want to.
To instruct loki that your adapter supports ‘reference’ mode, you will need to implement a top level property called ‘mode’ on your adapter and set it equal to ‘reference’. Having done that and configured that adapter to be used, whenever loki wishes to save the database it will instead call out to an exportDatabase() method on your adapter.
A simple example of an advanced ‘reference’ mode adapter might look like :
1 | function YourAdapter() { |
LokiPartitioningAdapter
This is an adapter for adapters. It wraps around and converts any ‘basic’ persistence adapter into one that scales nicely to your memory contraints. It can split your database up, saving each collection independently and only if changes have occurred since the last save. Since each collection is saved separately there is lower memory overhead and since only dirty collections are saved there is improved i/o save speeds.
Chrome (using indexedDb) places a restriction on how large a single saved ‘chunk’ can be, this Partitioning adapter with just partitioning raises that limit from being ‘per db’ to ‘per collection’… when paging is enabled that limit is raised to being ‘per document’. Chrome indexedDb limit is somewhere around 30-60megs sized chunks.
An example using partition adapter with our LokiIndexedAdapter might appear such as :
1 | var idbAdapter = new LokiIndexedAdapter('appAdapter'); |
If you expect a single collection to grow rather large you may even want to utilize an additional ‘paging’ mode that this adapter provides. This is useful if you want to limit the size of data sent to the inner persistence adapter. This paging mode was added to accomodate a Chrome limitation on maximum record sizes. An example using paging mode might appear as follows :
1 | var idbAdapter = new LokiIndexedAdapter('appAdapter'); |
You can also pass in a pageSize option if you wish to use a page size other than the default 25meg page size.
1 | // set up adapter to page using 35 meg page size |
LokiMemoryAdapter
This ‘basic’ persistence adapter is only intended for experimenting and testing since it retains its key/value store in memory and will be lost when session is done. This enables us to verify the partitioning adapter works and can be used to mock persistence for unit testing.
You might access this memory adapter (which is included in the main source file) similarly to the following :
1 | var mem = new loki.LokiMemoryAdapter(); |
If you wish to simulate asynchronous ‘basic’ adapter you can pass options to its constructor :
1 | // simulate 50ms async delay for loads and saves. this will yield thread until then |
In order to see LokiPartitioningAdapter used in conjunction with LokiMemoryAdapter you can view this Loki Sandbox gist in your browser.
What is happening in the gist linked above is that we create an instance of a LokiMemoryAdapter and pass that instance to the LokiPartitioningAdapter. We utilimately pass in the created LokiPartitioningAdapter instance to the database constructor. We then add multiple collections to our database, save it, update one of the collections (causing that collection’s ‘dirty’ flag to be set), and save again. When we examine the output of the script we can view the contents of the memory adapter’s internal hash store to see how there are multiple keys for a single database. We can also see that our modified collection (along with the database container itself) was saved again. The database container currently has no ‘dirty’ flag set but since we remove all collection.data[] object instances from it, it is relatively lightweight.
‘Rolling your own’ structured serialization mechanism
In addition to the ChangesAPI which can be utilized to isolate changesets, LokiJS has established several internal utility methods to assist users in developing optimal persistence or transmission of database contents.
Those mechanisms include the ability to decompose the database into ‘partitions’ of structured serializations or assembled into a line oriented format (non-partitioned) and either delimited (single delimited string per collection) or non-delimited (array of strings, one per document). These utility methods are located on the Loki object instance itself as the ‘serializeDestructured’ and ‘deserializeDestructured’ methods. They can be invoked to create structured json serialization for the entire database, or (if you pass a partition option) it can provide a single partition at a time. Internal loki structured serialization in its current form provides mild memory overhead reduction and decreases I/O time if only some collections need to be saved. It may also be useful for other data exchange or synchronization mechanisms.
In lokijs terminology the partitions of a database include the database container (partition -1) along with each individual collection (partitions 0-n).
To destructure in various formats you can experiment with the following parameters :
1 | var result = db.serializeDestructured({ |
To destructure a single partition you might use the following syntax and experiment with ‘delimited’ and ‘partition’ properties :
1 | var result = db.serializeDestructured({ |
To experiment with the various structured serialization formats you can view this Loki Sandbox gist and try various combinations of ‘partitioned’ and ‘delimited’ options (making sure both the serializeDestructured and deserializeDestructured use the same values.
Destructuring (making many smaller json serializations vs one large serialization) does not lower memory overhead but seems to be a little faster. Partitioning can reduce memory overhead if you can dispose of those memory chunks before advancing to the next (which our adapter implementations do). Our 2.0.0 branch which is able to use ES6 language constructs may gain an iterable interface in the future for data exchange or line-by-line streaming.
If your database is small enough you can use the LokiPartitioningAdapter (with or without paging) along with LokiMemoryAdapter to decompose database into appropriately sized ‘chunks’ for transmission.
Detailed LokiIndexedAdapter Description
Our LokiIndexedAdapter is implemented as a ‘basic’ mode loki persistence adapter. Since this will probably be the default web persistence adapter, this section will overview some of its advanced features.
It implements persistence by defining an app/key/value database in indexeddb for storing serialized databases (or partitions). The ‘app’ portion is designated when instantiating the adapter and loki only supplies it key/value pair for storage.
Simple Example of using LokiIndexedAdapter (for browser environments) :
1 | <script src="scripts/lokijs/lokijs.js"></script> |
…
1 | var idbAdapter = new LokiIndexedAdapter('finance'); |
Note the ‘finance’ in this case represents an ‘App’ context and the ‘test’ designates the key (or database name)… the ‘value’ is the serialized strings representing your database which loki will provide. Advantages include larger storage limits over localstorage, and a catalog based approach where you can store many databases, grouped by an ‘App’ context. Since indexedDB storage is provided ‘per-domain’, and on any given domain you might be running several web ‘apps’ each with its own database(s), this structure allows for organization and expandibility.
*Note : the ‘App’ context is an conceptual separation, not a security partition. Security is provided by your web browser, partitioned per-domain within client storage in the browser/system.*
Loki Indexed adapter interface
In addition to core loadDatabase and saveDatabase methods, the loki Indexed adapter provides the ability to getDatabaseList (for the current app context), deleteDatabase, and getCatalogSummary to retrieve unfiltered list of app/keys along with the size in database. (Note sizes reported may not be Unicode sizes so effective ‘size’ it may consume might be double that amount as indexeddb saves in Unicode). The loki indexed adapter also is console-friendly… even though indexeddb is highly asynchronous, relying on callbacks, you can omit callbacks for many of its methods and will log results to console instead. This makes experimenting, diagnosing, and maintenance of loki catalog easier to learn and inspect.
Full Examples of using loki indexed adapter
1 | // Save : will save App/Key/Val as 'finance'/'test'/{serializedDb} |
Examples of using loki Indexed adapter from console
1 | // CONSOLE USAGE : if using from console for management/diagnostic, here are a few examples : |