dwgebler/doclite

基于 SQLite 构建的高性能 NoSQL 数据库

1.1.9 2024-04-22 14:28 UTC

This package is auto-updated.

Last update: 2024-09-19 22:11:16 UTC


README

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

Build Status

目录

关于 DocLite

DocLite 是一个基于 SQLite 构建的 PHP NoSQL 文档存储,它利用 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,并轻松查询和过滤所需数据。

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

  • 用于在本地环境中安装和运行的 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() 以启用记录超过500毫秒的查询。这些查询将以 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 类中的公共常量表示。
错误代码的完整列表如下

导入和导出数据

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

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

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

导入数据

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

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文档;禁用同步可能导致崩溃或断电时数据丢失。

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

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

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

设置回滚日志模式

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

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

调用$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,或者首次实例化文档时将为您创建一个。自动生成的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);

查询集合

Collection对象提供了一系列方法来根据任意标准查找文档。

按值查找单个文档

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

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

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

$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 的缓存功能来加速复杂查询。

查询运算符

高级查询支持以下运算符

连接集合

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

例如,如果您有一个 users 集合和一个 comments 集合,其中 comments 集合中的一些文档包含一个字段 user_id。您可以查询 users 并与 comments 连接,这样任何匹配相同用户 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();

最后,可以将 Database 对象设置为在查询缓存时自动修剪过期条目。默认情况下,此行为是禁用的;要启用自动修剪,请在对数据库对象的 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 类表示,但您也可以从集合中创建或检索文档并将它们映射到您自己的类上。有关详细信息,请参阅 Collection 文档。

获取和设置文档数据

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 模式并移除验证。

$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'

您现在可以像任何其他服务一样类型提示 DatabaseInterface,如果希望使用 MemoryDatabase,则可以使用别名 $memoryDb 作为参数名称。

许可

DocLite 作为开源软件,遵循 MIT 许可。

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

Donate

错误和问题

如果您在项目 GitHub 上遇到任何问题,请提出问题。我总是希望改进软件。

联系作者

您可以发送邮件到 info@doclite.co.uk