cacko/doclite

基于 SQLite 的强大 NoSQL 数据存储

该软件包的官方仓库似乎已不存在,因此软件包已被冻结。

维护者

详细信息

github.com/cacko/doclite

源代码

1.1.6.3 2023-03-30 07:58 UTC

This package is auto-updated.

Last update: 2023-07-30 08:44:58 UTC


README

基于 SQLite 的强大 PHP NoSQL 文档存储。

Build Status

目录

关于 DocLite

DocLite 是基于 SQLite 的强大 NoSQL 文档存储,适用于 PHP。它使用 PHP PDO SQLite 库访问 SQLite 数据库,并自动管理以命名集合形式组织的文档,这些集合以 JSON 格式存储。

DocLite 利用 SQLite JSON1 扩展(通常包含在您 PHP 分发版的 libsqlite 中,因此您可能已经拥有它)来存储、解析、索引和查询 JSON 文档,从而提供完全事务性和 ACID 兼容的 NoSQL 解决方案的全部功能和灵活性,同时包含在本地文件系统中。当您的需求简单时,无需更复杂的系统,如 Mongo、CouchDB 或 Elasticsearch。无需任何外部依赖,只需启用 PDO SQLite 的 PHP 即可。

DocLite 提供了一个简单、直观、灵活且强大的 PHP 库,您可以在几分钟内学习、安装并开始使用。

为什么选择 DocLite?

DocLite 适用于各种用例,包括但不限于

  • 在需求不断演变的情况下,进行敏捷开发和快速原型设计。

  • 适用于小型到中型网站或应用的强大、自包含的 NoSQL 数据库,例如博客、商业网站、CMS、CRM 或论坛。

  • 用于从远程数据库、API 或服务器检索数据的快速、可靠的缓存。将数据转换为文档,保存在 DocLite 中,并轻松按需查询和筛选数据。

  • 用于替换较弱、较慢、基于 JSON、XML 或 YAML 的平面文件数据存储的强大、高性能、ACID 兼容的替代品。

  • 本地环境中安装和运行的 Web 应用程序的应用数据库。

  • 微服务和中间件的数据库。

  • 用于数据处理或机器学习算法的快速内存数据库。

总的来说,DocLite适用于与其构建在之上的SQLite引擎相同的使用场景,但当你需要NoSQL解决方案时。

入门

系统要求
  • PHP 7.4或更高版本

  • 启用PDO SQLite,基于libsqlite ≥ 3.18.0和JSON1扩展构建。

(在大多数系统中,如果您运行的是PHP 7.4,您可能已经满足第二个要求)

安装

使用Composer安装

composer require dwgebler/doclite

使用概述

DocLite提供了FileDatabaseMemoryDatabase的实现。要创建或打开现有数据库,只需创建一个Database对象,如果使用FileDatabase,请指定文件路径。

如果您的FileDatabase不存在,它将被创建(确保您的脚本具有适当的写入权限)。这包括创建所需的任何父目录。

如果您指定了不带文件名的现有目录,将使用默认文件名data.db

use Gebler\Doclite\{FileDatabase, MemoryDatabase};

// To create or open an existing file database.
$db = new FileDatabase('/path/to/db');

// To open an existing file database in read-only mode.
$db = new FileDatabase('/path/to/existing/db', true);

// To create a new in-memory database.
$db = new MemoryDatabase();

一旦打开数据库,您就可以获取一个文档Collection,如果它不存在,则会自动创建。

$users = $db->collection("user"); 

然后可以使用Collection对象检索、创建和操作文档。

// Create a new User in the collection
$user = $users->get();

// Get the automatically generated document ID
$id = $user->getId();

// Set properties by magic set* methods
$user->setUsername("dwgebler");
$user->setRole("admin");
$user->setPassword(password_hash("admin", \PASSWORD_DEFAULT));
$user->setCreated(new \DateTimeImmutable);

// Update the user in the collection
$user->save();

// Retrieve this user later on by ID
$user = $users->get($id);

// Or search for a user by any field
$user = $users->findOneBy(["username" => "dwgebler"]);

在上面的示例中,$user是DocLite Document的实例,但您也可以从集合中恢复您自己的自定义类对象。

class CustomUser
{
    private $id;
    private $username;
    private $password;
    
    public function getId() {...}
    public function setId($id) {...}
    public function getUsername() {...}
    public function setUsername($username) {...}    
}

// Retrieve a previously created user and map the result on to a CustomUser object.
// You can also pass a null ID as the first parameter to create a new CustomUser.
$user = $users->get($id, CustomUser::class);

// $user is now an instance of CustomUser and can be saved through the Collection.
$users->save($user);

要了解更多关于Collection对象的信息,包括如何查询文档存储,请阅读下面的完整文档。

数据库

DocLite建立在SQLite 3之上,支持两种类型的数据库;文件和内存。相应的类是FileDatabaseMemoryDatabase

创建内存数据库

MemoryDatabase存储在易失性内存中,因此对于您的应用程序脚本的整个生命周期都是临时的。它的构造函数接受可选参数

  • 一个布尔标志,表示是否启用全文搜索功能(默认为false) - 此功能要求SQLite已编译带有FTS5扩展
  • 一个表示最大连接超时秒数的整数(默认为1),即连接应等待多长时间,如果底层SQLite数据库被锁定。
  • 一个符合PSR-3的记录实例(默认为null)。
```php
use Gebler\Doclite\MemoryDatabase;

$db = new MemoryDatabase();

// With full text search enabled and a 2-second connection timeout
$logger = new \Monolog\Logger('my-logger');
$db = new MemoryDatabase(true, 2, $logger); 

创建文件数据库

FileDatabase构造函数接受一个必需参数和一些可选参数;仅需要指向新或现有数据库的文件或目录路径。

可选参数包括

  • 一个布尔标志,表示数据库是否应以只读模式打开,默认为false
  • 一个布尔标志,表示是否启用全文搜索功能(默认为false) - 此功能要求SQLite已编译带有FTS5扩展
  • 一个表示最大连接超时秒数的整数(默认为1),即连接应等待多长时间,如果底层SQLite数据库被锁定。
  • 一个用于记录数据库事件的PSR-3 Logger实例。

提供给FileDatabase的路径可以是相对路径或绝对路径,可以是以下之一

  • 具有读/写访问权限的现有目录。
  • 目录中不存在的文件,该目录具有读/写访问权限。
  • 目录中的现有数据库,该目录具有读/写或只读访问权限(只读模式)。
  • 一个不存在的目录路径,您的脚本有权创建。

如果没有指定文件名,将使用默认文件名 data.db 作为底层数据库。

use Gebler\Doclite\FileDatabase;

// Open a new database
$db = new FileDatabase('./data/mydb.db');

// Open an existing database in read-only mode
$db = new FileDatabase('./data/mydb.db', true);

// Open a new database called data.db in existing directory /home/data
$db = new FileDatabase('/home/data');

// All options - path, read-only mode, full text search, connection timeout and logger
$logger = new \Monolog\Logger('mylogger');
$db = new FileDatabase('./data/mydb.db', false, true, 1, $logger);

// Or, in PHP 8, named parameters:
$db = new FileDatabase(path: './data/mydb.db', readOnly: true, ftsEnabled: true);

如果您以只读模式打开数据库,您将能够从集合中检索文档,但无法保存它们或创建新的文档或集合。尝试这样做将触发错误。

FileDatabase 创建包装在 try-catch 块中是良好的实践。初始化 FileDatabase 可能会抛出 IOException(与文件系统相关的错误)或 DatabaseException(与建立数据库连接相关的错误)。

use Gebler\Doclite\Exception\IOException;
use Gebler\Doclite\Exception\DatabaseException;
use Gebler\Doclite\FileDatabase;

try {
  $db = new FileDatabase('/path/to/db');
} catch (IOException $e) {
    var_dump($e->getMessage());
} catch (DatabaseException $e) {
    var_dump($e->getMessage());
}

错误处理 & 记录

要启用记录发送到数据库的完整和最终 SQL 查询和参数,请通过构造函数或任何时间点的 $database->setLogger(LoggerInterface $logger) 方法传递 PSR-3 日志记录器实例到您的 FileDatabaseMemoryDatabase

然后调用 $database->enableQueryLogging() 以启用记录 所有 查询。这些将在 debug 级别记录。

或者调用 $database->enableSlowQueryLogging() 以启用记录执行时间超过 500ms 的查询。这些将以 warning 级别记录。

只要在数据库上设置了 LoggerInterface 实例,任何异常也将以 error 级别记录。

您可以通过调用 $database->disableQueryLogging()$database->disableSlowQueryLogging() 来禁用记录。

DocLite 主要在发生任何错误时抛出 DatabaseException。这在 DatabaseCollectionDocument 类型中都是如此。数据库异常将包括一条消息、一个错误代码(见下文)、如果有任何底层系统异常(因此直到这一点的正常 Exception 行为),以及正在执行的任何 SQL 查询(当然,DocLite 在正常操作期间会隐藏这些查询,因为它是一个 NoSQL 解决方案,但它们对于提交错误报告很有用!)以及任何相关参数的数组 - 这些可能是文档 ID、文档数据等。

use Gebler\Doclite\Exception\DatabaseException;
...
try {
    $user->setUsername("dwgebler");
    $user->save();
} catch (DatabaseException $e) {
    var_dump($e->getMessage(), $e->getCode(), $e->getQuery(), $e->getParams());
}

DatabaseException 可能在任何与底层数据库交互的 DatabaseCollectionDocument 方法上发生。

错误代码由 DatabaseException 类中的公共常量表示。
错误代码的完整列表如下

常量 含义
ERR_COLLECTION_IN_TRANSACTION 尝试在另一个集合的事务正在进行时开始、回滚或提交集合。
ERR_CONNECTION 无法连接到数据库
ERR_NO_SQLITE PDO SQLite 扩展未安装
ERR_NO_JSON1 SQLite 未安装 JSON1 扩展
ERR_NO_FTS5 FTS5 扩展未安装
ERR_INVALID_COLLECTION 集合名称无效
ERR_MISSING_ID_FIELD 用于映射文档的自定义类没有 ID 字段
ERR_INVALID_FIND_CRITERIA 尝试通过非标量值查找文档
ERR_INVALID_ID_FIELD 指定的自定义类唯一 ID 字段不存在,也没有默认值
ERR_ID_CONFLICT 同一集合中的多个文档具有相同的 ID
ERR_CLASS_NOT_FOUND 用于文档的自定义类名称不存在
ERR_INVALID_UUID 尝试从一个无效的 UUID 获取时间戳
ERR_QUERY 执行 SQL 查询出错
ERR_READ_ONLY_MODE 尝试在只读数据库上执行写操作
ERR_INVALID_JSON_SCHEMA 尝试导入无效的 JSON 模式
ERR_INVALID_DATA 数据与加载的 JSON 模式不匹配
ERR_MAPPING_DATA 无法将文档映射到类
ERR_IMPORT_DATA 导入数据出错
ERR_IN_TRANSACTION 在事务中进行锁定操作尝试
ERR_INVALID_TABLE 尝试访问无效的表
ERR_UNIQUE_CONSTRAINT 尝试插入一个具有已存在唯一字段的文档

导入和导出数据

DocLite可以从JSON、YAML、XML和CSV文件导入和导出数据。为此,Database对象提供了两个方法,import()export()

⚠️ 在非常大的集合上进行导入或导出操作可能会耗尽内存。这个特性将(可能)在未来得到改进,并使处理大型数据集更加高效。

建议对打算重新加载到DocLite数据库中的导出使用JSON。对其他格式的支持是实验性的。

导入数据

您想导入的数据可以组织成文件,每个文件代表多个文档的集合,或者一个目录,其中每个子目录代表一个集合,包含代表单个文档的多个文件。

import(string $path, string $format, int $mode)

format可以是jsonyamlxmlcsv中的任何一个。这也应该与包含您的数据的文件名的扩展名相匹配。

当使用csv格式时,CSV文件的第一行假定为包含字段名的标题行。

mode可以是常量Database::MODE_IMPORT_COLLECTIONSDatabase::MODE_IMPORT_DOCUMENTS之一。

集合名称是从子目录或文件名推断出来的。例如,/path/to/collections/users.json将导入到users集合,同样,当从多个文件导入集合时,子目录/path/to/collections/users/也将导入到users集合。

// Create a new, empty database
$db = new FileDatabase('/path/to/data.db');

// Import the contents of a directory where each file is a collection
$db->import('/path/to/collections', 'json', Database::IMPORT_COLLECTIONS);

// Import the contents of a directory where each sub directory is a collection 
// of files representing single documents.
$db->import('/path/to/collections', 'json', Database::IMPORT_DOCUMENTS);

当您将文档导入到集合中时,任何与数据库中现有文档的唯一ID匹配的文档都将覆盖该文档。否则,将为任何不匹配或缺失ID的文档创建新文档。

💡 每个集合的导入都将被单个事务包装,因此每个集合的导入都是原子性的。您还可以通过设置以下高级选项来加快批量导入的速度,以更改数据库的同步和回滚日志模式,使其更加宽松,如果您理解这样做的影响。

导出数据

您可以将一个或多个集合的全部内容导出。与导入数据类似,您可以选择DocLite是否将此作为每个集合包含多个文档的一个文件导出,或者每个集合一个目录,每个文档一个文件。

export(string $path, string $format, int $mode, array $collections = [])

format可以是jsonyamlxmlcsv中的任何一个。

mode可以是常量Database::MODE_EXPORT_COLLECTIONSDatabase::MODE_EXPORT_DOCUMENTS之一。

collections可以是一组集合名称字符串和/或Collection对象。如果为空,则导出数据库中的所有集合。

// Export the entire database to one file per collection in the specified 
// output directory.
$db->export('/path/to/export', 'json', Database::EXPORT_COLLECTIONS);

// Export the entire database to a directory structure with one file per document.
$db->export('/path/to/export', 'json', Database::EXPORT_DOCUMENTS);

// Export only the "User" and "Person" collections.
// Assume Collection $persons = $db->get("Person");
$db->export(
    '/path/to/export', 
    'json',
    Database::EXPORT_COLLECTIONS,
    ['User', $persons]
);

⚠️ XML标准对实体名称施加了一些限制。当导出到此格式时,DocLite将用下划线替换文档字段中的任何无效字符。这意味着您可能无法在导入这些文件到DocLite数据库后,完全恢复您的文档存储状态。

高级选项

DocLite Database对象有一些方法用于更高级的选项。

获取DocLite版本

// Return the version of DocLite as a SemVer string, e.g. 1.0.0
$db->getVersion();

优化数据库

调用 $db->optimize() 尝试数据库优化。此函数不返回任何内容,但如果发生错误,可能会抛出 DatabaseException。定期优化可以减少数据库文件大小并提高性能。

设置同步模式

Database 类中,可以设置 SQLite 的底层同步模式为以下常量之一。有关更改此值的详细影响,请参阅 SQLite 文档;禁用同步可能导致在崩溃或断电时数据丢失。

常量 含义
MODE_SYNC_OFF 禁用同步
MODE_SYNC_NORMAL 常规同步 默认设置
MODE_SYNC_FULL 完全同步
MODE_SYNC_EXTRA 额外同步

调用 $db->setSyncMode(Database::MODE_CONSTANT) 来设置模式。例如,要设置完全同步模式,请调用 $db->setSyncMode(Database::MODE_SYNC_FULL)

此函数在成功时返回 true,在失败时返回 false

调用 $db->getSyncMode() 获取当前模式,该模式可以与一个常量进行比较。返回类型是 int

设置回滚日志模式

可以设置 SQLite 的底层回滚日志管理为以下常量之一。有关更改此值的详细影响,请参阅 SQLite 文档;禁用回滚日志可能导致意外的数据状态。

⚠️ 警告:如果您禁用回滚日志,事务、原子提交和回滚将不再工作。在此模式下,对集合上的事务方法的行为是未定义的,可能导致不可预测的结果或数据损坏。因此,请不要在 MODE_JOURNAL_NONE 中使用 事务方法

常量 含义
MODE_JOURNAL_NONE 禁用回滚日志
MODE_JOURNAL_MEMORY 仅内存回滚日志
MODE_JOURNAL_WAL 使用写入前日志 默认设置
MODE_JOURNAL_DELETE 在每个事务结束时删除回滚日志
MODE_JOURNAL_TRUNCATE 在每个事务结束时截断回滚日志
MODE_JOURNAL_PERSIST 防止删除回滚日志

调用 $db->setJournalMode(Database::MODE_CONSTANT) 来设置模式。
例如,要设置 WAL 模式,请调用 $db->setJournalMode(Database::MODE_JOURNAL_WAL)

此函数在成功时返回 true,在失败时返回 false

调用 $db->getJournalMode() 获取当前模式,该模式可以与一个常量进行比较。返回类型是 string

集合

关于集合

集合是 DocLite 的核心。一个 Collection 表示一组命名的文档(例如,“用户”)并类似于结构化数据库中的表。

💡 注意:集合在底层 SQLite 数据库中表示为表。它们必须遵循一些规则

  • 集合名称不能以 sqlite_ 开头
  • 集合名称不能以数字开头。
  • 集合名称可以包含仅包含字母数字字符和下划线。
  • 集合名称的长度不能超过 64 个字符。

Collection 对象是您创建、查找、更新和删除文档的方式。

集合中的每个文档都必须有一个唯一的 ID。您可以自己提供它,或者当您第一次实例化文档时,将为您创建一个。自动生成的 ID 采用 v1 UUID 的形式,其中包括文档首次创建时的时戳。

获取集合

通过调用 collection 方法,从 FileDatabaseMemoryDatabase 获取集合。如果集合不存在,它将被自动创建。

$userCollection = $db->collection("Users");

创建文档

一旦你有了集合,通过调用集合的 get 方法来创建一个新的文档。

$newUser = $userCollection->get();

保存文档

默认情况下,文档以 DocLite Document 对象的形式返回,该对象提供了 save() 方法。你也可以通过在集合上调用 save() 并将文档对象作为参数,来保存任何类型的文档。

// works for DocLite Document objects
$newUser->save();

// works for both DocLite documents and documents mapped to custom types
$userCollection->save($newUser);

检索文档

get 也可以用来通过 ID 检索文档。

$existingUser = $userCollection->get($id);

将文档映射到自定义类

默认情况下,检索文档将返回一个 DocLite Document 对象,该对象提供了魔法方法和属性,以便您访问和操作文档数据。然而,也可以创建或检索任何自定义类的文档对象,只要该类具有您希望填充的文档字段公开属性或 getter/setter 方法。

// Get a user as an object of type CustomUser.
$user = $userCollection->get($id, CustomUser::class);

默认情况下,DocLite 会查找一个名为 id 的属性来填充文档的唯一 ID。如果您想在自定义类上使用不同的属性来存储此 ID,例如,因为您的类没有 id 属性,或者您正在将其用于其他目的,您可以在 get 的第三个参数中指定自定义 ID 属性名称。

$user = $userCollection->get($id, CustomUser::class, 'databaseId');

或者,您可以在类中添加一个名为 docliteId 的公共属性或 getter/setter,如果不存在 id 属性,DocLite 将自动尝试填充此属性。

虽然 Document 类提供了一个内置的 save() 方法来方便地更新存储中的文档,但表示为您的自定义类的文档必须通过集合对象来保存。

$userCollection->save($user);

如果您在类上使用自定义属性来保存文档的唯一 ID,您应该提供 ID 作为附加参数。

$userCollection->save($user, $user->getDatabaseId());

最后,在保存表示为自定义类的文档时,您可以指定一个可选的第三个参数来列出您不希望在文档中存储的对象属性。只有在您希望排除的属性是公共的 / 有 getter/setter 方法,或者公共 get 方法不代表属性时,才需要进行此操作。

$userCollection->save($user, $user->getDatabaseId(), ['nonDatabaseField']);

删除文档

save() 类似,DocLite Document 对象上有一个方便的 delete() 方法,集合本身也有一个 deleteDocument(object $document) 方法。

// Works for DocLite Document objects.
$user->delete();

// works for both DocLite documents and documents mapped to custom types
$userCollection->deleteDocument($user);

查询集合

集合对象提供了一系列方法,可以根据任意标准查找文档。

通过值查找单个文档

通过调用 findOneBy 查找所有键值与指定值匹配的单个文档。

$user = $userCollection->findOneBy([
    'role' => 'admin',
    'name' => 'Mr Administrator',
]);

findOneBy 方法接受可选的自定义类名和自定义类ID字段参数,与 get 方法的用法相同。

$user = $userCollection->findOneBy(['username' => 'admin'], CustomUser::class, 'databaseId');

如果找不到匹配的文档,则返回 null

通过值查找所有匹配的文档

findAllBy 函数的用法与 findOneBy 相同,但会返回一个生成器,您可以对其进行迭代,或通过PHP的 iterator_to_array 函数将其转换为数组。

foreach($userCollection->findAllBy(['active' => true]) as $user) {
   ...
}

在集合中查找所有文档

要检索集合中的所有文档,请使用 findAll() 方法。与前面的两个函数一样,findAll 可以接受可选的自定义类名和ID属性作为参数。

foreach($userCollection->findAll() as $user) {
   ...
}

高级查询

DocLite 包含一个强大的查询构建机制,可以检索或删除与任意标准匹配的集合中的所有文档。

要构建查询,请在对集合对象使用 where()and()or()limit()offset()orderBy() 函数的任何组合后调用 fetch()delete()count()

您还可以运行嵌套查询,通过 union()(用于通过 OR 对子句进行分组)和 intersect()(用于通过 AND 对子句进行分组)将子句组合在一起。

您可以通过使用 . 点字符分隔嵌套字段来查询文档的任何深度,也可以在列表字段的末尾添加方括号 [] 以查询列表中所有匹配的值。

通过示例可以更好地理解高级查询API。

对于以下代码片段,假设您的用户集合中的每个文档都类似于以下数据示例,此处以YAML表示。

示例用户文档
username: adamjones
first_name: Adam
last_name: Jones
password: "$2y$10$LRS.0xUCJjWSmQuWMMRsuurZ0OGlU.NH7KYXsipzkfUa0YREEarj2"
address:
  street: 123 Fake Street
  area: Testville
  county: Testshire
  postcode: TE1 3ST
roles:
- USER
- EDITOR
telephone: "+441234567890"
registered: true
active: true
lastLogin: "2021-02-13T10:34:40+00:00"
email: adamjones@example.com
api_access:
  "/v1/pages/":
  - POST
  - GET
  "/v1/contributors/":
  - GET

以下是一些可以对这些文档集合运行的示例查询。

示例查询
$users = $db->collection("Users");

$activeUsers = $users->where('active', '=', true)->fetch();

$gmailUsers = $users->where('email', 'ENDS', '@gmail.com')->fetch();

$registeredAndNotActiveUsers = $users->where('registered', '=', true)
                                     ->and('active', '=', false)
                                     ->fetch();

$usersInPostalArea = $users->where('address.postcode', 'STARTS', 'TE1')->fetch();

$usersWith123InPhone = $users->where('telephone', 'CONTAINS', '123')->fetch();

$usersWithNoNumbersInUsername = $users->where('username', 'MATCHES', '^[A-Za-z]*$')
                                       ->fetch();
                                       
$usersWithEditorRole = $users->where('roles[]', '=', 'EDITOR')->fetch();

$usersWithEditorOrAdminRole = $users->where('roles[]', '=', 'ADMIN')
                                    ->or('roles[]', '=', 'EDITOR')
                                    ->fetch();
                                    
$usersWithEditorAndAdminRole = $users->where('roles', '=', ['ADMIN', 'EDITOR']);                                    
                                    
$usersWhoHaveAtLeastOneRoleWhichIsNotAdmin = $users->where('roles[]', '!=', 'ADMIN')->fetch();

/* 
 * This next one is trickier. "roles" is a list of values in our document.
 * As we can see above, roles[] != ADMIN would return all users who
 * have at least one role in their list which is not ADMIN.
 * But this means if a user has roles ["USER","ADMIN"], they would
 * be matched.
 * So for users who do NOT have the ADMIN role at all, we can
 * quote the value "ADMIN" and ask for matches where the entire list of roles
 * (so no square brackets) does not contain this value.
*/
$usersDoNotHaveAdminRole = $users->where('roles', 'NOT CONTAINS', '"ADMIN"')->fetch();

$deleteAllUsersWithEditorRole = $users->where('roles[]', '=', 'EDITOR')->delete();

$first10UsersOrderedByFirstName = $users->orderBy('first_name', 'ASC')
                                        ->limit(10)
                                        ->fetch();                                                               

$next10UsersOrderedByFirstName = $users->orderBy('first_name', 'ASC')
                                       ->limit(10)
                                       ->offset(10)
                                       ->fetch();                                                               

// Use [] on any field which is a list to search within its sub-items
$usersWithPostAccessToPagesApi = $users->where(
    'api_access./v1/pages/[]', '=', 'POST')->fetch();                                     

$allUsersWithPostAccessToAnyApi = $users->where('api_access[]', '=', 'POST')
                                        ->fetch();

$start = new DateTimeImmutable('2021-03-01 00:00:00');
$end = new DateTime('2021-06-30');
$usersWhoSignedUpBetweenMarchAndJune = $users->where('date', 'BETWEEN', $start, $end)->fetchArray();                                     

/**
 * Nested queries are also possible.
 * To get all users where 
 * (active=true and address.postcode matches '^[A-Za-z0-9 ]*$')
 * OR
 * (roles[] list contains "EDITOR" and lastLogin > 2021-01-30)
 */
$nestedUsers = $users->where('active', '=', true)
                     ->and('address.postcode', 'MATCHES', '^[A-Za-z0-9 ]*$')
                     ->union()
                     ->where('roles[]', '=', 'EDITOR')
                     ->and('lastLogin', '>', '2021-01-30')
                     ->fetch();

💡 与 findAllBy() 一样,fetch() 方法返回一个生成器,而不是一个数组。如果您想一次性获取所有结果,请将 fetch() 替换为 fetchArray()

💡 高级查询的 fetch() 方法可以接受自定义类名和自定义ID字段作为可选参数,这与 findOneByfindAllByfindAll() 方法类似。

💡 启用 DocLite 的缓存功能可以加快复杂查询的速度。

查询运算符

高级查询支持以下运算符

运算符 含义
= 等于,精确匹配
!= 不等于
< 小于
> 大于
<= 小于或等于
>= 大于或等于
BETWEEN 介于两个值之间,包含。相当于 >= AND <=
NOT BETWEEN 不在两个值之间,包含。相当于 < OR >
STARTS 文本以...开头
NOT STARTS 文本不以...开头
ENDS 文本以...结尾
NOT ENDS 文本不以...结尾
CONTAINS 文本包含
NOT CONTAINS 文本不包含
MATCHES 文本正则表达式匹配
NOT MATCHES 文本负正则表达式匹配
EMPTY 没有值,null
NOT EMPTY 有任何值,非null

连接集合

在查询时可以将一个集合与一个或多个其他集合连接起来,以包括返回的文档中这些集合的匹配结果。这类似于关系数据库中的外键。

例如,如果您有一个users集合和一个comments集合,其中comments集合中的一些文档包含一个字段user_id。您可以对users进行查询并与其进行连接,这样任何与相同用户ID匹配的comments中的文档都将包含在users文档中,在名为comments的字段下。

/**
 * Imagine a user document like:
 * {"__id":"1", "name":"John Smith"}
 * 
 * and a corresponding comments document like:
 * {"__id":"5", "user_id": "1", "comment":"Hello world!"} 
 * 
 * You can query the users collection with a join to retrieve an aggregated document like this:
 * {"__id":"1","name":"John Smith","comments":[{"__id":"5","comment":"Hello world!"}]}
 */
$users = $db->collection('Users');
$comments = $db->collection("Comments");
$users->where('__id', '=', '1')->join($comments, 'user_id', '__id')->fetchArray();

Collection::join方法接受要连接的集合作为第一个参数,该集合中用作外键的文档字段名称作为第二个参数,以及连接集合(例如,用户)中对应的字段以匹配。

因此,上述示例是在查找Comments中的文档,其中字段user_id与用户中的字段__id匹配。

由于joinCollection标准查询构建接口的一部分,因此您可以将其与其他查询运算符(如whereand等)或其他连接组合。

缓存结果

DocLite可以缓存查询结果以加快复杂结果集的检索速度。然而,对于非常简单的查询,这可能不会提供任何好处,甚至可能带来轻微的性能损失,因此您只需在需要时才打开它。

要为集合打开缓存,请调用集合对象的enableCache()方法。

同样,您可以通过调用disableCache()来禁用缓存。

缓存结果有效期为缓存生存期,默认为60秒。您可以通过调用setCacheLifetime($seconds)来更改缓存有效期。缓存生存期为零表示缓存结果永远不会过期。

您可以通过调用clearCache()来手动刷新缓存。

$userCollection->enableCache();

// Set the cache validity period to 1 hour.
$userCollection->setCacheLifetime(3600);

$userCollection->disableCache();

$userCollection->clearCache();

最后,数据库对象可以设置为在查询缓存时自动删除过期条目。默认情况下,此行为是禁用的;要启用自动修剪,请在数据库对象上调用enableCacheAutoPrune()

$db->enableCacheAutoPrune();

💡 对于复杂的查询,缓存非常快。如果您正在对大量大型数据集执行大量复杂查询,并且这些查询可能会重复进行而数据在缓存生存期内不会发生变化,那么使用DocLite的缓存是一个好主意。

索引集合

可以在集合内的任何文档字段上建立索引以加快对该字段的查询。

创建集合时,会自动为内部ID字段添加索引。要添加自定义索引,请使用文档字段的名称调用addIndex方法。

$userCollection->addIndex('email');

要添加单个索引在多个字段(如多列索引)上,只需将附加字段名称作为单独的参数调用addIndex

$userCollection->addIndex('first_name', 'last_name');

唯一索引

您还可以添加唯一索引,该索引作为字段上的约束,确保集合中不会有两个文档具有相同字段或字段组合的值。

$userCollection->addUniqueIndex('email');

如果您尝试向具有唯一索引的集合添加一个包含已存在于集合中的值的文档,将抛出带有代码DatabaseException::ERR_UNIQUE_CONSTRAINTDatabaseException

💡 注意:索引是高级功能,其工作方式与其他任何SQLite数据库中的索引相同,唯一的区别是它们是在文档字段上而不是在表列上创建。选择不当的索引可能不会提供任何好处,甚至可能减慢查询。

删除集合

要完全删除集合中的所有文档,请调用 deleteAll() 方法。

$userCollection->deleteAll();

集合事务

可以在事务中包装一系列数据库操作。为此,请使用 CollectionbeginTransaction()commit()rollback() 方法。

$collection = $db->collection("Users");
$collection->beginTransaction();

// ...do some stuff, insert a bunch of records or whatever...

// commit the results and end the transaction
$collection->commit();

// or rollback the changes and end the transaction
$collection->rollback();

全文搜索

DocLite 能够针对集合构建强大的全文索引,允许您搜索并生成按相关性排序的文档列表,其中指定的字段匹配某些文本或短语。

全文搜索功能需要您的 PHP 的 libsqlite 使用 FTS5 扩展构建。就像 JSON1 扩展一样,这通常包含在标准发行版中,所以您可能已经拥有它。

要搜索集合,请确保已将您的 Database 初始化为将全文参数设置为 true 以启用此功能,然后只需在任何集合上调用 search() 方法,后跟搜索短语和一个包含您希望搜索的任何文档字段名称的数组。

$path = '/path/to/db';
$readOnly = false;
$ftsEnabled = true;
$timeout = 1;
$db = new FileDatabase($path, $readOnly, $ftsEnabled, $timeout);
$blogPosts = $db->collection("posts");
$results = $blogPosts->search('apache', ['title', 'summary', 'content']);

结果将自动按相关性排序。

💡 DocLite 将智能管理您的全文索引以保持数据库优化。当您调用 search() 时,如果搜索的字段集合没有索引,它将在第一次搜索时自动创建。如果您稍后对现有索引的字段超集调用 search(),原始索引将被销毁,并创建一个包含所有搜索字段的新的大索引。这样 DocLite 就可以使用可能的最小索引来搜索 所有 您希望搜索的字段。

在小型集合中,此过程非常快,您可能看不到任何影响。但是,如果您有一个非常大的集合,建议您通过从单独的脚本中调用一次 search() 来创建全文索引,这样当您的应用程序首次运行并调用 search() 时,相关的索引已经存在。

因为 search() 是集合标准查询获取接口的一部分(与 fetch()count() 相同),所以它可以用 where()and() 等正常查询过滤器提前。与 fetch() 类似,search() 方法返回一个生成器。您可以使用 PHP 的 iterator_to_array() 函数将结果转换为数组。

文档

关于文档

文档是数据的形式,为键值对,以 JSON 格式存储在数据库中;也就是说,集合内的每个文档都可以有自己的自由结构。这并不重要,是否与同一集合中任何其他文档的结构匹配,这取决于您的应用程序如何决定使用 DocLite。

默认情况下,文档将由 DocLite 的 Document 类表示,但是也可以从集合中创建或检索文档并将它们映射到您自己的类。有关详细信息,请参阅集合文档。

获取和设置文档数据

Document 类提供了用于任意文档键的魔法获取和设置方法以及属性访问器。也就是说,一旦您从 Collection 获取了 Document,您就可以通过这两种方法设置或读取您喜欢的任何属性

$users = $db->collection("Users");
// Create a new Document with an auto generated UUID.
$user = $db->get();
// Create a new property called username via a magic setter.
$user->setUsername('dwgebler');
// Create a new property called password via a magic property.
$user->password = password_hash("admin", \PASSWORD_DEFAULT);
// Read the username property via a magic property.
echo $user->username;
// Read the password property via a magic getter.
echo $user->getPassword();
// Properties can contain scalar values, arrays, or even other Documents and
// custom objects.
$user->setRoles(['user', 'admin']);

魔法方法和属性访问技术之间存在一个小的语义差异;当使用魔法方法时,属性名称将从 camelCase 转换为 snake_case,而直接属性访问则是字面量,例如。

// setter uses camel case
$user->setFirstName('Dave');

// but the corresponding property created will be lower cased and snake_cased
echo $user->first_name;

// if you want a key in a document to be case sensitive, set it as a property only
$user->FirstName = 'Dave';

// you should now use the property access to retrieve its value later on
echo $user->FirstName;

// This will not work and will raise a ValueError on getFooBar(),
// because the method call will look for a property called foo_bar
$user->FooBar = 'baz';
$user->getFooBar();

《Document》类还提供了两个额外的方法,getValuesetValue,用于通过点 . 符号表示的路径查询文档中的嵌套键。这些方法也可以用来获取或设置无法通过魔法设置方法或属性表示的名称的字段。

getValue() 如果指定的路径找不到,将引发 ValueError

setValue() 将自动在嵌套路径上创建任何父属性。

// This is the same as:
// $address = $user->getAddress();
// $postcode = $address['postcode'];
$user->getValue('address.postcode');

// Assume "roles" is a list, this will return an array
$user->getValue('roles');

// Retrieve the first role
$user->getValue('roles.0');

// Assume api_access is a dictionary of keys mapped to lists.
// This will return the list of data under the /v1/users/ key
// as an array.
$access = $user->getValue('api_access./v1/users/');
if (!in_array('POST', $access)) { ... }

// If address does not exist, it will be created with postcode as a key.
$user->setValue('address.postcode', 'TE1 3ST');

// Or set a value with special characters in the name:
$user->setValue('api_access./v1/users/', ['GET', 'POST']);

💡 文档字段的值是任意的。标量值、数组甚至自定义类的对象都可以存储在文档中。

将文档字段映射到对象

如果您已将文档作为默认的 Document 对象检索,则仍然可以将表示自定义对象的文档字段映射到自定义类。为此,请使用 Documentmap() 方法,并传递一个字段名称(可以使用上面描述的嵌套点 . 表示法),以及如果您想填充现有对象,则传递一个类名称或现有对象实例。

假设您在应用程序中有一个以下自定义类

示例类
class Person
{
    private $id;

    private $firstName;

    private $lastName;

    private $address = [];

    private $postcode;

    private $dateOfBirth;

    private $identityVerified;

    public function getId(): ?int
    {
        return $this->id;
    }
    
    public function setId(string $id): self
    {
        $this->id = $id;
        return $this;
    }    

    public function getFirstName(): ?string
    {
        return $this->firstName;
    }

    public function setFirstName(string $firstName): self
    {
        $this->firstName = $firstName;
        return $this;
    }

    public function getLastName(): ?string
    {
        return $this->lastName;
    }

    public function setLastName(string $lastName): self
    {
        $this->lastName = $lastName;
        return $this;
    }

    public function getAddress(): ?array
    {
        return $this->address;
    }

    public function setAddress(array $address): self
    {
        $this->address = $address;
        return $this;
    }

    public function getPostcode(): ?string
    {
        return $this->postcode;
    }

    public function setPostcode(string $postcode): self
    {
        $this->postcode = $postcode;
        return $this;
    }

    public function getDateOfBirth(): ?\DateTimeImmutable
    {
        return $this->dateOfBirth;
    }

    public function setDateOfBirth(\DateTimeImmutable $dateOfBirth): self
    {
        $this->dateOfBirth = $dateOfBirth;
        return $this;
    }

    public function getIdentityVerified(): ?bool
    {
        return $this->identityVerified;
    }

    public function setIdentityVerified(bool $identityVerified): self
    {
        $this->identityVerified = $identityVerified;
        return $this;
    }
}

以及一个具有以下结构的 User 文档

示例文档
__id: b83e319a-7887-11eb-8deb-b9e03d2e720d
username: daniel_johnson1
active: false
roles:
- CONTRIBUTOR
- AUTHOR
telephone: "+441254220959364"
password: "$2y$10$y8P2Cjph1F.iIc.s2j9aM.GW9qy8aOMeEfzDulQox465mgBJF.pPG"
person:
  firstName: Daniel
  lastName: Johnson
  address:
    house: '123'
    street: Test Road
    city: Testville
    Country: Testshire
  postcode: "TE1 3ST"
  dateOfBirth: '1980-03-23T00:00:00+00:00'
  identityVerified: true

当您最初检索 Document 时,person 键将包含一个数组。但您可以将其映射到您的 Person 类,如下所示

$user = $collection->get("b83e319a-7887-11eb-8deb-b9e03d2e720d");
$user->map('person', Person::class);

// $user->getPerson() now returns a Person object.

// Or you can map to an existing Person object.
$person = new Person();
$user->map('person', $person);

文档唯一 ID

同一集合中的每个文档都必须有一个唯一的 ID。

默认情况下,当您创建一个新的文档时,会为您生成一个作为 v1 UUID 的 ID。

您可以使用 getId()setId(string $id) 方法获取或设置 Document ID。

💡 注意:更改 Document ID 实质上将其视为不同的文档,即提供新的唯一 ID 将在保存时在数据库中插入新文档。同样,将文档的 ID 更改为集合中另一个文档的 ID 将覆盖该文档。

如果 ID 是自动生成的,您可以通过调用其 getTime() 方法来获取表示文档创建时间的 DateTimeImmutable

$users = $db->collection("Users");
// Create a new Document with an auto generated UUID.
$user = $users->get();
// $date is a \DateTimeImmutable
$date = $user->getTime();
echo $date->format('d m Y H:i');

如果您不想为新文档使用自动生成的 ID,只需将您自己的 ID 传递给集合的 get() 方法。只要该 ID 与集合数据库存储中的任何文档不匹配,就会创建新文档。文档 ID 是字符串。

$users = $db->collection("Users");
// Create a new Document with a custom ID.
// If this ID already exists in the Users collection, that document will be returned.
$user = $users->get("user_3815");

保存文档

表示为 DocLite Document 对象的文档提供了一个方便的方法来将其保存到其集合。要保存存储中的 Document,请调用 save()

$users = $db->collection("Users");
$user = $users->get();
$user->setUsername("admin");
$user->save();

如果已将文档映射到自定义类,则需要通过其集合保存它。

$users = $db->collection("Users");
// Create a new document with an automatically generated UUID and
// retrieved as an object of type CustomUser.
$user = $users->get(null, CustomUser::class);
$user->setUsername("admin");
$users->save($user);

删除文档

表示为 DocLite Document 对象的文档提供了一个方便的方法来将其从其集合中删除。要删除存储中的 Document,请调用 delete()

$users = $db->collection("Users");
$user = $users->get("12345");
$user->delete();

如果已将文档映射到自定义类,则需要通过其集合删除它。

$users = $db->collection("Users");
// Create a new document with an automatically generated UUID and
// retrieved as an object of type CustomUser.
$user = $users->get("12345", CustomUser::class);
$users->deleteDocument($user);

文档验证

可以通过 addJsonSchema() 方法将 JSON Schema 验证添加到 Document。这需要一个有效的 JSON schema 字符串作为单一参数。如果无法验证模式,将抛出 DatabaseException

$user->addJsonSchema(file_get_contents('schema.json'));

加载模式后,每次设置文档属性或尝试保存文档时,文档数据都将与您的模式进行验证。如果数据验证失败,将抛出DatabaseException异常。

您还可以通过调用validateJsonSchema()在任何时间手动进行验证。

$user->addJsonSchema(file_get_contents('schema.json'));
try {
    $user->validateJsonSchema();
    // This will automatically call validateJsonSchema() anyway.
    $user->save();
    // As will this.
    $user->setUsername("foobar");
} catch (DatabaseException $e) {
    $params = $e->getParams();
    $error = $params['error'];
    echo "Document failed to validate against JSON Schema because:\n".$error;
}

最后,您可以通过调用removeJsonSchema()卸载JSON Schema并移除验证。

$user->removeJsonSchema();

其他信息

Symfony集成

尽管没有与Symfony框架的特定集成,但将DocLite注入到任何Symfony应用程序中非常简单。只需通过Composer将DocLite作为应用程序依赖项安装,然后根据以下示例修改您的services.yaml

    app.filedatabase:
        class: Gebler\Doclite\FileDatabase
        arguments:
            $path: "../var/data/app.db"
            $readOnly: false
    app.memorydatabase:
        class: Gebler\Doclite\MemoryDatabase

    Gebler\Doclite\DatabaseInterface: '@app.filedatabase'
    Gebler\Doclite\DatabaseInterface $memoryDb: '@app.memorydatabase'

现在,您可以使用别名$memoryDb作为参数名称,像其他服务一样为DatabaseInterface进行类型提示,如果您想使用MemoryDatabase

许可

DocLite作为开源软件,可在MIT许可下获得。

如果您使用DocLite并认为它很有用,我非常感激对其实际发展的任何支持。

Donate

错误,问题

如果您遇到任何问题,请在项目的GitHub上提出一个问题。我总是致力于改进软件。

联系作者

您可以通过info@doclite.co.uk给我发邮件